Navigation: present search as a sheet from library (avoids nested NavigationStack), use view-based NavigationLink for song rows (fixes double-push from duplicate navigationDestination). Translation: Apple Translation inserts a blank line after every translated line. Strip all blanks from the EN output, then re-insert them at the same positions where the original ES has blanks. Result is 1:1 line pairing between Spanish and English. Store reset: revert localStoreResetVersion bump — adding SavedSong is a lightweight SwiftData migration, no store nuke needed. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
97 lines
2.9 KiB
Swift
97 lines
2.9 KiB
Swift
import SwiftUI
|
|
import SharedModels
|
|
import SwiftData
|
|
|
|
struct LyricsLibraryView: View {
|
|
@Query(sort: \SavedSong.savedDate, order: .reverse) private var songs: [SavedSong]
|
|
@State private var showingSearch = false
|
|
|
|
var body: some View {
|
|
Group {
|
|
if songs.isEmpty {
|
|
ContentUnavailableView(
|
|
"No Songs Yet",
|
|
systemImage: "music.note.list",
|
|
description: Text("Tap + to search for Spanish song lyrics.")
|
|
)
|
|
} else {
|
|
List {
|
|
ForEach(songs) { song in
|
|
NavigationLink {
|
|
LyricsReaderView(song: song)
|
|
} label: {
|
|
SongRowView(song: song)
|
|
}
|
|
}
|
|
.onDelete(perform: deleteSongs)
|
|
}
|
|
}
|
|
}
|
|
.navigationTitle("Lyrics")
|
|
.navigationBarTitleDisplayMode(.large)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarTrailing) {
|
|
Button {
|
|
showingSearch = true
|
|
} label: {
|
|
Image(systemName: "plus")
|
|
}
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingSearch) {
|
|
NavigationStack {
|
|
LyricsSearchView {
|
|
showingSearch = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Environment(\.modelContext) private var modelContext
|
|
|
|
private func deleteSongs(at offsets: IndexSet) {
|
|
for index in offsets {
|
|
modelContext.delete(songs[index])
|
|
}
|
|
try? modelContext.save()
|
|
}
|
|
}
|
|
|
|
// MARK: - Song Row
|
|
|
|
private struct SongRowView: View {
|
|
let song: SavedSong
|
|
|
|
var body: some View {
|
|
HStack(spacing: 12) {
|
|
if !song.albumArtURL.isEmpty, let url = URL(string: song.albumArtURL) {
|
|
AsyncImage(url: url) { image in
|
|
image.resizable().scaledToFill()
|
|
} placeholder: {
|
|
RoundedRectangle(cornerRadius: 6)
|
|
.fill(.fill.quaternary)
|
|
}
|
|
.frame(width: 50, height: 50)
|
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
|
} else {
|
|
Image(systemName: "music.note")
|
|
.font(.title2)
|
|
.foregroundStyle(.secondary)
|
|
.frame(width: 50, height: 50)
|
|
.background(.fill.quaternary, in: RoundedRectangle(cornerRadius: 6))
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(song.title)
|
|
.font(.subheadline.weight(.semibold))
|
|
.lineLimit(1)
|
|
Text(song.artist)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
.padding(.vertical, 2)
|
|
}
|
|
}
|