Files
Spanish/Conjuga/Conjuga/Views/Practice/Lyrics/LyricsSearchView.swift
Trey t 636193fae1 Fix lyrics navigation, translation line alignment, and store reset
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>
2026-04-11 23:08:32 -05:00

180 lines
5.5 KiB
Swift

import SwiftUI
import SharedModels
import SwiftData
import Translation
struct LyricsSearchView: View {
var onSaved: (() -> Void)?
@Environment(\.modelContext) private var modelContext
@Environment(\.dismiss) private var dismiss
@State private var artist = ""
@State private var songTitle = ""
@State private var isSearching = false
@State private var searchResults: [LyricsSearchResult] = []
@State private var errorMessage: String?
@State private var selectedResult: LyricsSearchResult?
private let service = LyricsSearchService()
var body: some View {
List {
searchSection
if isSearching {
loadingSection
}
if let errorMessage {
errorSection(errorMessage)
}
if !searchResults.isEmpty {
resultsSection
}
}
.navigationTitle("Search Lyrics")
.navigationBarTitleDisplayMode(.inline)
.navigationDestination(item: $selectedResult) { result in
LyricsConfirmationView(result: result) {
if let onSaved {
onSaved()
} else {
dismiss()
}
}
}
}
// MARK: - Sections
private var searchSection: some View {
Section {
TextField("Artist", text: $artist)
.textInputAutocapitalization(.words)
.autocorrectionDisabled()
TextField("Song Title", text: $songTitle)
.textInputAutocapitalization(.words)
.autocorrectionDisabled()
Button {
performSearch()
} label: {
HStack {
Spacer()
Label("Search", systemImage: "magnifyingglass")
.font(.headline)
Spacer()
}
}
.disabled(artist.trimmingCharacters(in: .whitespaces).isEmpty ||
songTitle.trimmingCharacters(in: .whitespaces).isEmpty ||
isSearching)
} header: {
Text("Find a Song")
}
}
private var loadingSection: some View {
Section {
HStack {
Spacer()
ProgressView("Searching...")
Spacer()
}
.padding(.vertical, 8)
}
}
private func errorSection(_ message: String) -> some View {
Section {
Label(message, systemImage: "exclamationmark.triangle")
.foregroundStyle(.red)
}
}
private var resultsSection: some View {
Section {
ForEach(Array(searchResults.prefix(5).enumerated()), id: \.offset) { _, result in
Button {
selectedResult = result
} label: {
HStack(spacing: 12) {
if let artURL = result.albumArtURL, let url = URL(string: artURL) {
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)
.frame(width: 50, height: 50)
.background(.fill.quaternary, in: RoundedRectangle(cornerRadius: 6))
}
VStack(alignment: .leading, spacing: 2) {
Text(result.title)
.font(.subheadline.weight(.semibold))
Text(result.artist)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundStyle(.tertiary)
}
}
.tint(.primary)
}
} header: {
Text("Results")
}
}
// MARK: - Actions
private func performSearch() {
isSearching = true
errorMessage = nil
searchResults = []
Task {
do {
let results = try await service.searchLyrics(artist: artist, title: songTitle)
searchResults = results
if results.isEmpty {
errorMessage = "No lyrics found. Try a different spelling."
}
} catch {
errorMessage = "Search failed. Check your connection."
}
isSearching = false
}
}
}
// MARK: - Identifiable conformance for navigation
extension LyricsSearchResult: Equatable {
static func == (lhs: LyricsSearchResult, rhs: LyricsSearchResult) -> Bool {
lhs.title == rhs.title && lhs.artist == rhs.artist && lhs.lyricsES == rhs.lyricsES
}
}
extension LyricsSearchResult: Hashable {
func hash(into hasher: inout Hasher) {
hasher.combine(title)
hasher.combine(artist)
}
}
extension LyricsSearchResult: Identifiable {
var id: String { "\(artist)\(title)" }
}