SwiftUI app against master API: link, profiles, browse, live TV, favorites sync, and TestFlight-ready assets. Co-authored-by: Cursor <cursoragent@cursor.com>
101 lines
3.7 KiB
Swift
101 lines
3.7 KiB
Swift
import SwiftUI
|
|
|
|
struct SearchScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
@State private var query = ""
|
|
@State private var results: [MediaItem] = []
|
|
@State private var error: String?
|
|
@State private var searching = false
|
|
|
|
private let cols = [GridItem(.adaptive(minimum: 104), spacing: 12)]
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "magnifyingglass")
|
|
.foregroundStyle(McColors.textDim)
|
|
TextField("Films, series…", text: $query)
|
|
.textInputAutocapitalization(.never)
|
|
.autocorrectionDisabled()
|
|
.foregroundStyle(McColors.text)
|
|
.submitLabel(.search)
|
|
.onSubmit { Task { await runSearch() } }
|
|
if !query.isEmpty {
|
|
Button {
|
|
query = ""
|
|
results = []
|
|
} label: {
|
|
Image(systemName: "xmark.circle.fill")
|
|
.foregroundStyle(McColors.textDim)
|
|
}
|
|
}
|
|
}
|
|
.padding(12)
|
|
.background(McColors.surface, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
|
|
.padding(.horizontal, 16)
|
|
.padding(.top, 8)
|
|
.padding(.bottom, 12)
|
|
|
|
if searching {
|
|
ProgressView().tint(.white).padding(.top, 40)
|
|
Spacer()
|
|
} else if let error {
|
|
Text(error).foregroundStyle(McColors.danger).padding()
|
|
Spacer()
|
|
} else if results.isEmpty && query.count >= 2 {
|
|
ContentUnavailableView("Geen resultaten", systemImage: "magnifyingglass")
|
|
} else if results.isEmpty {
|
|
ContentUnavailableView("Zoeken in VMC", systemImage: "sparkle.magnifyingglass", description: Text("Typ minstens 2 tekens."))
|
|
} else {
|
|
ScrollView {
|
|
LazyVGrid(columns: cols, spacing: 14) {
|
|
ForEach(results) { item in
|
|
NavigationLink(value: item) {
|
|
PosterCard(item: item, width: 104)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.bottom, 24)
|
|
}
|
|
.navigationDestination(for: MediaItem.self) { MediaRouter(item: $0) }
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(McColors.bg.ignoresSafeArea())
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .principal) {
|
|
BrandHeader(subtitle: "ZOEKEN")
|
|
}
|
|
}
|
|
.toolbarBackground(McColors.bg, for: .navigationBar)
|
|
.toolbarColorScheme(.dark, for: .navigationBar)
|
|
.onChange(of: query) { _, newValue in
|
|
Task {
|
|
try? await Task.sleep(nanoseconds: 350_000_000)
|
|
guard newValue == query else { return }
|
|
await runSearch()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func runSearch() async {
|
|
let q = query.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard q.count >= 2 else {
|
|
results = []
|
|
error = nil
|
|
return
|
|
}
|
|
searching = true
|
|
error = nil
|
|
do {
|
|
results = try await app.api.search(query: q)
|
|
searching = false
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
searching = false
|
|
}
|
|
}
|
|
}
|