import SwiftUI struct SearchView: View { @EnvironmentObject var serverManager: ServerManager @EnvironmentObject var audioPlayer: AudioPlayer @EnvironmentObject var offlineManager: OfflineManager @ObservedObject private var libraryCache = LibraryCache.shared @State private var searchText = "" @State private var artists: [Artist] = [] @State private var albums: [Album] = [] @State private var songs: [Song] = [] @State private var isSearching = false private let accentPink = Color(red: 1.0, green: 0.176, blue: 0.333) var body: some View { NavigationStack { ScrollView { VStack(spacing: 0) { // Search bar HStack(spacing: 10) { Image(systemName: "magnifyingglass") .foregroundColor(.gray) TextField("Artists, Songs, Albums", text: $searchText) .foregroundColor(.white) .autocapitalization(.none) .disableAutocorrection(true) .onSubmit { performSearch() } .onChange(of: searchText) { oldValue, newValue in if newValue.count >= 2 { performSearch() } } if !searchText.isEmpty { Button(action: { searchText = "" artists = []; albums = []; songs = [] }) { Image(systemName: "xmark.circle.fill") .foregroundColor(.gray) } } } .padding(10) .background(Color.white.opacity(0.08)) .cornerRadius(10) .padding(.horizontal, 16) .padding(.top, 10) if isSearching { ProgressView().tint(accentPink).padding(.top, 40) } else if !searchText.isEmpty { searchResults } else { emptyState } Color.clear.frame(height: 120) } } .background(Color(white: 0.06)) .navigationTitle("Search") } } private var emptyState: some View { VStack(spacing: 12) { Image(systemName: "magnifyingglass") .font(.system(size: 40)) .foregroundColor(.gray) Text("Search your library") .font(.system(size: 16)) .foregroundColor(.gray) } .padding(.top, 80) } private var searchResults: some View { LazyVStack(alignment: .leading, spacing: 0) { // Artists if !artists.isEmpty { sectionHeader("Artists") ForEach(artists) { artist in NavigationLink(destination: ArtistDetailView(artistId: artist.id)) { HStack(spacing: 12) { AsyncCoverArt(coverArtId: artist.coverArt, size: 44) .frame(width: 44, height: 44) .clipShape(Circle()) Text(artist.name) .font(.system(size: 15)) .foregroundColor(.white) Spacer() Image(systemName: "chevron.right") .font(.system(size: 12)) .foregroundColor(.gray) } .padding(.horizontal, 16) .padding(.vertical, 8) } } } // Albums if !albums.isEmpty { sectionHeader("Albums") ForEach(albums) { album in NavigationLink(destination: AlbumDetailView(albumId: album.id)) { HStack(spacing: 12) { AsyncCoverArt(coverArtId: album.coverArt, size: 48) .frame(width: 48, height: 48) .cornerRadius(4) VStack(alignment: .leading, spacing: 2) { Text(album.name) .font(.system(size: 15)) .foregroundColor(.white) .lineLimit(1) Text(album.artist ?? "") .font(.system(size: 12)) .foregroundColor(.gray) } Spacer() Image(systemName: "chevron.right") .font(.system(size: 12)) .foregroundColor(.gray) } .padding(.horizontal, 16) .padding(.vertical, 8) } } } // Songs if !songs.isEmpty { sectionHeader("Songs") ForEach(songs) { song in let available = offlineManager.isSongDownloaded(song.id) || !libraryCache.isOffline Button(action: { if available { audioPlayer.play(song: song, fromQueue: songs) } }) { HStack(spacing: 12) { AsyncCoverArt(coverArtId: song.coverArt, size: 44) .frame(width: 44, height: 44) .cornerRadius(3) .opacity(available ? 1.0 : 0.4) VStack(alignment: .leading, spacing: 2) { Text(song.title) .font(.system(size: 15)) .foregroundColor( !available ? .gray.opacity(0.35) : audioPlayer.currentSong?.id == song.id ? accentPink : .white ) .lineLimit(1) Text("\(song.artist ?? "") — \(song.album ?? "")") .font(.system(size: 12)) .foregroundColor(available ? .gray : .gray.opacity(0.3)) .lineLimit(1) } Spacer() Text(song.durationFormatted) .font(.system(size: 13)) .foregroundColor(available ? .gray : .gray.opacity(0.3)) } .padding(.horizontal, 16) .padding(.vertical, 8) } } } } } private func sectionHeader(_ title: String) -> some View { Text(title) .font(.system(size: 20, weight: .bold)) .foregroundColor(.white) .padding(.horizontal, 16) .padding(.top, 20) .padding(.bottom, 8) } private func performSearch() { guard searchText.count >= 2 else { return } isSearching = true Task { do { let result = try await serverManager.client.search3(query: searchText) await MainActor.run { artists = result?.artist ?? [] albums = result?.album ?? [] songs = result?.song ?? [] isSearching = false } } catch { await MainActor.run { isSearching = false } } } } }