Wire FAAB schedule-events into master-api and admin IPTV settings, add Android TV Events UI, fix mobile ClearKey/stream playback (DASH + bump 1.0.23), and scaffold the Apple TV (VMCTV) client with docs/scripts.
97 lines
3.6 KiB
Swift
97 lines
3.6 KiB
Swift
import SwiftUI
|
|
|
|
struct SearchView: 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: 168, maximum: 200), spacing: 24)
|
|
]
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 24) {
|
|
Text("Zoeken")
|
|
.font(TvFonts.title)
|
|
.foregroundStyle(TvColors.text)
|
|
.padding(.horizontal, 48)
|
|
.padding(.top, 12)
|
|
|
|
TextField("Films, series…", text: $query)
|
|
.font(TvFonts.headline)
|
|
.foregroundStyle(TvColors.text)
|
|
.padding(.horizontal, 28)
|
|
.padding(.vertical, 20)
|
|
.background(.ultraThinMaterial, in: RoundedRectangle(cornerRadius: 18, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
|
.strokeBorder(Color.white.opacity(0.12), lineWidth: 1)
|
|
)
|
|
.padding(.horizontal, 48)
|
|
.onSubmit { Task { await runSearch() } }
|
|
|
|
Group {
|
|
if searching {
|
|
ProgressView().tint(.white).frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else if let error {
|
|
Text(error).font(TvFonts.body).foregroundStyle(TvColors.danger).padding(.horizontal, 48)
|
|
Spacer()
|
|
} else if results.isEmpty && query.count >= 2 {
|
|
Text("Geen resultaten")
|
|
.font(TvFonts.headline)
|
|
.foregroundStyle(TvColors.textDim)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else if results.isEmpty {
|
|
Text("Typ minstens 2 tekens om te zoeken.")
|
|
.font(TvFonts.body)
|
|
.foregroundStyle(TvColors.textDim)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
} else {
|
|
ScrollView {
|
|
LazyVGrid(columns: cols, spacing: 28) {
|
|
ForEach(results) { item in
|
|
NavigationLink(value: item) {
|
|
PosterCard(item: item, width: 168)
|
|
}
|
|
.buttonStyle(FocusableCardStyle())
|
|
}
|
|
}
|
|
.padding(.horizontal, 48)
|
|
.padding(.bottom, 48)
|
|
}
|
|
.focusSection()
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(TvColors.bg.ignoresSafeArea())
|
|
.navigationDestination(for: MediaItem.self) { MediaRouter(item: $0) }
|
|
.onChange(of: query) { _, newValue in
|
|
Task {
|
|
try? await Task.sleep(nanoseconds: 400_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
|
|
}
|
|
}
|
|
}
|