From 636193fae1fceff25201e6c33ef85e10e3c3f346 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 11 Apr 2026 23:08:32 -0500 Subject: [PATCH] Fix lyrics navigation, translation line alignment, and store reset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Conjuga/Conjuga/ConjugaApp.swift | 2 +- .../Lyrics/LyricsConfirmationView.swift | 58 ++++++++++++++++++- .../Practice/Lyrics/LyricsLibraryView.swift | 17 ++++-- .../Practice/Lyrics/LyricsReaderView.swift | 9 +++ .../Practice/Lyrics/LyricsSearchView.swift | 8 ++- 5 files changed, 85 insertions(+), 9 deletions(-) diff --git a/Conjuga/Conjuga/ConjugaApp.swift b/Conjuga/Conjuga/ConjugaApp.swift index 0e3649d..ff8883e 100644 --- a/Conjuga/Conjuga/ConjugaApp.swift +++ b/Conjuga/Conjuga/ConjugaApp.swift @@ -226,7 +226,7 @@ struct ConjugaApp: App { /// Clears accumulated stale schema metadata from previous container configurations. /// Bump the version number to force another reset if the schema changes again. private static func performOneTimeLocalStoreResetIfNeeded(at url: URL) { - let resetVersion = 3 // bump: added SavedSong to local schema + let resetVersion = 2 // bump: widget schema moved to SharedModels let key = "localStoreResetVersion" let defaults = UserDefaults.standard diff --git a/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsConfirmationView.swift b/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsConfirmationView.swift index 939e2b7..561845f 100644 --- a/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsConfirmationView.swift +++ b/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsConfirmationView.swift @@ -134,16 +134,70 @@ struct LyricsConfirmationView: View { private func translateLyrics(session: sending TranslationSession) async { await MainActor.run { isTranslating = true } let text = result.lyricsES + let esLines = text.components(separatedBy: "\n") + print("[LyricsTranslation] Spanish line count: \(esLines.count)") + print("[LyricsTranslation] Spanish blank lines: \(esLines.filter { $0.trimmingCharacters(in: .whitespaces).isEmpty }.count)") + print("[LyricsTranslation] First 10 ES lines:") + for (i, line) in esLines.prefix(10).enumerated() { + print(" ES[\(i)]: \(line.isEmpty ? "(blank)" : line)") + } + do { let response = try await session.translate(text) - await MainActor.run { translatedEN = response.targetText } + let enLines = response.targetText.components(separatedBy: "\n") + print("[LyricsTranslation] English line count: \(enLines.count)") + print("[LyricsTranslation] English blank lines: \(enLines.filter { $0.trimmingCharacters(in: .whitespaces).isEmpty }.count)") + print("[LyricsTranslation] First 10 EN lines:") + for (i, line) in enLines.prefix(10).enumerated() { + print(" EN[\(i)]: \(line.isEmpty ? "(blank)" : line)") + } + // The Translation framework often inserts a blank line after every + // translated line. Collapse consecutive blank lines into single blanks, + // then trim leading/trailing blanks so the EN structure matches the ES. + let normalized = Self.normalizeTranslationLineBreaks( + translated: response.targetText, + originalES: text + ) + let normalizedLines = normalized.components(separatedBy: "\n") + print("[LyricsTranslation] After normalization: EN lines \(enLines.count) → \(normalizedLines.count) (target: \(esLines.count))") + + await MainActor.run { translatedEN = normalized } } catch { - print("Translation error: \(error)") + print("[LyricsTranslation] Translation error: \(error)") await MainActor.run { translationError = true } } await MainActor.run { isTranslating = false } } + /// Re-align translated line breaks to match the original Spanish structure. + /// The Translation framework often inserts a blank line after every translated + /// line. We strip all blanks from the EN output, then re-insert them at the + /// same positions where the original ES text has blank lines. + /// Re-align translated line breaks to match the original Spanish structure. + private static func normalizeTranslationLineBreaks(translated: String, originalES: String) -> String { + let esLines = originalES.components(separatedBy: "\n") + let enContentLines = translated.components(separatedBy: "\n") + .filter { !$0.trimmingCharacters(in: .whitespaces).isEmpty } + + // Walk ES lines. For each blank ES line, insert a blank in the result. + // For each content ES line, consume the next EN content line. + var result: [String] = [] + var enIndex = 0 + for esLine in esLines { + if esLine.trimmingCharacters(in: .whitespaces).isEmpty { + result.append("") + } else if enIndex < enContentLines.count { + result.append(enContentLines[enIndex]) + enIndex += 1 + } else { + result.append("") + } + } + + print("[LyricsNormalize] ES lines: \(esLines.count), EN content: \(enContentLines.count), result: \(result.count), EN consumed: \(enIndex)") + return result.joined(separator: "\n") + } + private func saveSong() { let song = SavedSong( title: result.title, diff --git a/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsLibraryView.swift b/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsLibraryView.swift index 779fe50..1254a6d 100644 --- a/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsLibraryView.swift +++ b/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsLibraryView.swift @@ -4,6 +4,7 @@ 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 { @@ -16,7 +17,9 @@ struct LyricsLibraryView: View { } else { List { ForEach(songs) { song in - NavigationLink(value: song) { + NavigationLink { + LyricsReaderView(song: song) + } label: { SongRowView(song: song) } } @@ -28,15 +31,19 @@ struct LyricsLibraryView: View { .navigationBarTitleDisplayMode(.large) .toolbar { ToolbarItem(placement: .topBarTrailing) { - NavigationLink { - LyricsSearchView() + Button { + showingSearch = true } label: { Image(systemName: "plus") } } } - .navigationDestination(for: SavedSong.self) { song in - LyricsReaderView(song: song) + .sheet(isPresented: $showingSearch) { + NavigationStack { + LyricsSearchView { + showingSearch = false + } + } } } diff --git a/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsReaderView.swift b/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsReaderView.swift index 84e2b62..6fe4be3 100644 --- a/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsReaderView.swift +++ b/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsReaderView.swift @@ -56,6 +56,15 @@ struct LyricsReaderView: 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.. Void)? + @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss @@ -33,7 +35,11 @@ struct LyricsSearchView: View { .navigationBarTitleDisplayMode(.inline) .navigationDestination(item: $selectedResult) { result in LyricsConfirmationView(result: result) { - dismiss() + if let onSaved { + onSaved() + } else { + dismiss() + } } } }