import SwiftUI struct BrowseView: View { @EnvironmentObject private var app: AppModel let kind: String let title: String @State private var shelfPrefs: [ShelfPref] = [] @State private var catalogId: String = "" @State private var sort: SortMode = .title @State private var pageTitle: String = "" @State private var items: [MediaItem] = [] @State private var total = 0 @State private var listOffset = 0 @State private var error: String? @State private var loading = true @State private var loadingMore = false private enum SortMode: String { case title case recent } private var defaultCatalog: String { kind == "MOVIE" ? "movies" : "series" } private var catalogChips: [(label: String, id: String)] { var list: [(String, String)] = [] var seen = Set() func append(_ label: String, _ id: String) { guard !seen.contains(id) else { return } seen.insert(id) list.append((label, id)) } if kind == "MOVIE" { append("Alles", "movies") append("Recent", "recent-movies") append("4K", "4k-movies") } else { append("Alles", "series") append("Recent", "recent-series") } for pref in shelfPrefs where pref.visible { let k = (pref.kind ?? "").uppercased() let match = kind == "MOVIE" ? (k == "MOVIE" || k.contains("MOVIE")) : (k == "SERIES" || k.contains("SERIES")) if match { let slug = pref.slug?.isEmpty == false ? pref.slug! : pref.shelfId append(pref.name, slug) } } return list } private var showSortTools: Bool { switch catalogId { case "recent-movies", "recent-series": return false default: return true } } private let cols = [ GridItem(.adaptive(minimum: 168, maximum: 200), spacing: 24) ] var body: some View { VStack(alignment: .leading, spacing: 0) { catalogBar headerBar gridContent } .background(TvColors.bg.ignoresSafeArea()) .task { if catalogId.isEmpty { catalogId = defaultCatalog } await bootstrap() } .navigationDestination(for: MediaItem.self) { MediaRouter(item: $0) } } private var catalogBar: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 14) { ForEach(catalogChips, id: \.id) { chip in let selected = catalogId == chip.id Button { guard catalogId != chip.id else { return } catalogId = chip.id if chip.id.contains("recent") || chip.id == "4k-movies" { sort = .recent } else { sort = .title } Task { await reload(reset: true) } } label: { Text(chip.label) .font(TvFonts.caption) .foregroundStyle(selected ? .white : TvColors.text) .padding(.horizontal, 22) .padding(.vertical, 12) .background(selected ? TvColors.accent : TvColors.surfaceSoft, in: Capsule()) } .buttonStyle(FocusableCardStyle()) } } .padding(.horizontal, 48) .padding(.vertical, 16) } .focusSection() } private var headerBar: some View { HStack(spacing: 16) { Text(pageTitle.isEmpty ? title : pageTitle) .font(TvFonts.title) .foregroundStyle(TvColors.text) Text(total > 0 ? "\(total)" : (loading ? "…" : "0")) .font(TvFonts.body) .foregroundStyle(TvColors.textDim) Spacer() if showSortTools { HStack(spacing: 10) { sortButton("A–Z", selected: sort == .title) { sort = .title Task { await reload(reset: true) } } sortButton("Recent", selected: sort == .recent) { sort = .recent Task { await reload(reset: true) } } } } } .padding(.horizontal, 48) .padding(.bottom, 12) } private func sortButton(_ label: String, selected: Bool, action: @escaping () -> Void) -> some View { Button(action: action) { Text(label) .font(TvFonts.caption) .foregroundStyle(selected ? .black : TvColors.text) .padding(.horizontal, 20) .padding(.vertical, 10) .background(selected ? Color.white : TvColors.surface, in: Capsule()) } .buttonStyle(FocusableCardStyle()) } @ViewBuilder private var gridContent: some View { if loading && items.isEmpty { ProgressView().tint(.white).frame(maxWidth: .infinity, maxHeight: .infinity) } else if let error, items.isEmpty { VStack(spacing: 16) { Text(error).font(TvFonts.body).foregroundStyle(TvColors.danger) Button("Opnieuw") { Task { await reload(reset: true) } }.buttonStyle(TvPrimaryButtonStyle()) } .frame(maxWidth: .infinity, maxHeight: .infinity) } else if !loading && items.isEmpty { Text("Geen titels") .font(TvFonts.title) .foregroundStyle(TvColors.textDim) .frame(maxWidth: .infinity, maxHeight: .infinity) } else { ScrollView { LazyVGrid(columns: cols, spacing: 28) { ForEach(items) { item in NavigationLink(value: item) { PosterCard(item: item, width: 168, showTitle: true) } .buttonStyle(FocusableCardStyle()) .onAppear { if item.id == items.last?.id { Task { await loadMore() } } } } } .padding(.horizontal, 48) .padding(.vertical, 28) .padding(.bottom, 48) if loadingMore { ProgressView().tint(.white).padding(.bottom, 24) } } .scrollClipDisabled() .focusSection() } } private func bootstrap() async { shelfPrefs = (try? await app.api.getShelfPrefs())?.sorted(by: { $0.sortOrder < $1.sortOrder }) ?? [] await reload(reset: true) } private func reload(reset: Bool) async { if reset { loading = true error = nil } let id = catalogId.isEmpty ? defaultCatalog : catalogId let sortParam: String? = showSortTools ? sort.rawValue : nil do { let page = try await app.api.browse(catalogId: id, skip: 0, take: 60, sort: sortParam) pageTitle = page.title?.isEmpty == false ? page.title! : title total = page.total ?? page.items.count listOffset = page.offset ?? 0 items = page.items loading = false } catch { self.error = error.localizedDescription loading = false } } private func loadMore() async { guard !loadingMore, items.count + listOffset < total else { return } loadingMore = true let id = catalogId.isEmpty ? defaultCatalog : catalogId let sortParam: String? = showSortTools ? sort.rawValue : nil do { let page = try await app.api.browse( catalogId: id, skip: listOffset + items.count, take: 60, sort: sortParam ) let existing = Set(items.map(\.id)) items.append(contentsOf: page.items.filter { !existing.contains($0.id) }) loadingMore = false } catch { loadingMore = false } } }