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>
98 lines
3.4 KiB
Swift
98 lines
3.4 KiB
Swift
import SwiftUI
|
|
import SharedModels
|
|
|
|
struct LyricsReaderView: View {
|
|
let song: SavedSong
|
|
|
|
var body: some View {
|
|
ScrollView {
|
|
VStack(spacing: 20) {
|
|
headerSection
|
|
lyricsBody
|
|
}
|
|
.padding()
|
|
.adaptiveContainer()
|
|
}
|
|
.navigationTitle(song.title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
}
|
|
|
|
// MARK: - Header
|
|
|
|
private var headerSection: some View {
|
|
VStack(spacing: 10) {
|
|
if !song.albumArtURL.isEmpty, let url = URL(string: song.albumArtURL) {
|
|
AsyncImage(url: url) { image in
|
|
image.resizable().scaledToFill()
|
|
} placeholder: {
|
|
RoundedRectangle(cornerRadius: 12)
|
|
.fill(.fill.quaternary)
|
|
}
|
|
.frame(width: 160, height: 160)
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
|
|
Text(song.title)
|
|
.font(.title2.weight(.bold))
|
|
.multilineTextAlignment(.center)
|
|
|
|
Text(song.artist)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
|
|
if !song.appleMusicURL.isEmpty, let url = URL(string: song.appleMusicURL) {
|
|
Link(destination: url) {
|
|
Label("Open in Apple Music", systemImage: "apple.logo")
|
|
.font(.caption.weight(.medium))
|
|
}
|
|
.tint(.pink)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Lyrics Body
|
|
|
|
private var lyricsBody: some View {
|
|
let spanishLines = song.lyricsES.components(separatedBy: "\n")
|
|
let englishLines = song.lyricsEN.components(separatedBy: "\n")
|
|
let lineCount = max(spanishLines.count, englishLines.count)
|
|
let _ = {
|
|
print("[LyricsReader] ES lines: \(spanishLines.count), EN lines: \(englishLines.count), rendering: \(lineCount)")
|
|
for i in 0..<min(15, lineCount) {
|
|
let es = i < spanishLines.count ? spanishLines[i] : "(none)"
|
|
let en = i < englishLines.count ? englishLines[i] : "(none)"
|
|
print(" [\(i)] ES: \(es.isEmpty ? "(blank)" : es)")
|
|
print(" EN: \(en.isEmpty ? "(blank)" : en)")
|
|
}
|
|
}()
|
|
|
|
return VStack(alignment: .leading, spacing: 0) {
|
|
ForEach(0..<lineCount, id: \.self) { index in
|
|
let es = index < spanishLines.count ? spanishLines[index] : ""
|
|
let en = index < englishLines.count ? englishLines[index] : ""
|
|
|
|
if es.trimmingCharacters(in: .whitespaces).isEmpty &&
|
|
en.trimmingCharacters(in: .whitespaces).isEmpty {
|
|
// Blank line = section divider
|
|
Spacer().frame(height: 20)
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
if !es.isEmpty {
|
|
Text(es)
|
|
.font(.body.weight(.medium))
|
|
}
|
|
if !en.isEmpty {
|
|
Text(en)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
}
|
|
.padding()
|
|
.background(.fill.quinary, in: RoundedRectangle(cornerRadius: 12))
|
|
}
|
|
}
|