SavedSong was in the local container alongside reference data, so it got deleted whenever localStoreResetVersion was bumped. Move it to the cloud container (CloudKit-synced) so saved songs persist across schema changes. Update lyrics views to use cloudModelContextProvider. Closes #4 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
108 lines
3.3 KiB
Swift
108 lines
3.3 KiB
Swift
import SwiftUI
|
|
import SharedModels
|
|
import SwiftData
|
|
|
|
struct LyricsLibraryView: View {
|
|
@Environment(\.cloudModelContextProvider) private var cloudModelContextProvider
|
|
@State private var songs: [SavedSong] = []
|
|
@State private var showingSearch = false
|
|
|
|
private var cloudModelContext: ModelContext { cloudModelContextProvider() }
|
|
|
|
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
|
|
loadSongs()
|
|
}
|
|
}
|
|
}
|
|
.onAppear(perform: loadSongs)
|
|
}
|
|
|
|
private func loadSongs() {
|
|
let descriptor = FetchDescriptor<SavedSong>(
|
|
sortBy: [SortDescriptor(\SavedSong.savedDate, order: .reverse)]
|
|
)
|
|
songs = (try? cloudModelContext.fetch(descriptor)) ?? []
|
|
}
|
|
|
|
private func deleteSongs(at offsets: IndexSet) {
|
|
for index in offsets {
|
|
cloudModelContext.delete(songs[index])
|
|
}
|
|
try? cloudModelContext.save()
|
|
loadSongs()
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|