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)" } }