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() + } } } }