stremio/apps/ios/VMC/UI/BrowseScreen.swift
Jos Vooges | STH 252bce838b Add native VMC iOS client with VLC playback.
SwiftUI app against master API: link, profiles, browse, live TV, favorites sync, and TestFlight-ready assets.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-07 19:16:29 +02:00

193 lines
7.1 KiB
Swift

import SwiftUI
struct BrowseScreen: View {
@EnvironmentObject private var app: AppModel
let kind: String // "MOVIE" or "SERIES"
let title: String
@State private var home: HomePayload?
@State private var shelfPrefs: [ShelfPref] = []
@State private var mode: BrowseMode = .planks
@State private var items: [MediaItem] = []
@State private var total = 0
@State private var error: String?
@State private var loading = true
@State private var loadingMore = false
private enum BrowseMode: Hashable {
case planks
case catalog(String)
}
private var kindShelves: [ShelfRow] {
guard let home else { return [] }
return home.shelves.filter { shelf in
let k = (shelf.kind ?? "").uppercased()
if kind == "MOVIE" { return k == "MOVIE" || k.contains("MOVIE") }
return k == "SERIES" || k.contains("SERIES")
}
}
private var chips: [(label: String, mode: BrowseMode)] {
var list: [(String, BrowseMode)] = [("Planken", .planks)]
if kind == "MOVIE" {
list += [("Alles", .catalog("movies")), ("Recent", .catalog("recent-movies")), ("4K", .catalog("4k-movies"))]
} else {
list += [("Alles", .catalog("series")), ("Recent", .catalog("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
list.append((pref.name, .catalog(slug)))
}
}
return list
}
private let cols = [GridItem(.adaptive(minimum: 104), spacing: 12)]
var body: some View {
VStack(spacing: 0) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(Array(chips.enumerated()), id: \.offset) { _, chip in
Button {
mode = chip.mode
Task { await activate(chip.mode) }
} label: {
Text(chip.label)
.font(McFonts.caption)
.foregroundStyle(mode == chip.mode ? .white : McColors.textDim)
.padding(.horizontal, 14)
.padding(.vertical, 8)
.background(mode == chip.mode ? McColors.accent : McColors.surface, in: Capsule())
}
.buttonStyle(.plain)
}
}
.padding(.horizontal, 16)
.padding(.vertical, 10)
}
content
}
.background(McColors.bg.ignoresSafeArea())
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
BrandHeader(subtitle: title.uppercased())
}
}
.toolbarBackground(McColors.bg, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.task { await bootstrap() }
.refreshable { await bootstrap() }
}
@ViewBuilder
private var content: some View {
if loading && home == nil && items.isEmpty {
ProgressView().tint(.white).frame(maxWidth: .infinity, maxHeight: .infinity)
} else if let error, home == nil && items.isEmpty {
VStack(spacing: 12) {
Text(error).foregroundStyle(McColors.danger)
Button("Opnieuw") { Task { await bootstrap() } }.buttonStyle(McPrimaryButtonStyle())
}
} else {
switch mode {
case .planks:
ScrollView {
VStack(alignment: .leading, spacing: 22) {
if kindShelves.isEmpty {
ContentUnavailableView("Geen planken", systemImage: "rectangle.stack", description: Text("Nog geen \(title.lowercased())-planken zichtbaar."))
.frame(maxWidth: .infinity)
.padding(.top, 40)
} else {
ForEach(kindShelves) { shelf in
if !shelf.items.isEmpty {
ShelfSection(title: shelf.title, items: shelf.items)
}
}
}
}
.padding(.bottom, 24)
}
case .catalog:
ScrollView {
LazyVGrid(columns: cols, spacing: 14) {
ForEach(items) { item in
NavigationLink(value: item) {
PosterCard(item: item, width: 104)
}
.buttonStyle(.plain)
.onAppear {
if item.id == items.last?.id { Task { await loadMore() } }
}
}
}
.padding(.horizontal, 16)
.padding(.bottom, 24)
if loadingMore { ProgressView().tint(.white).padding() }
}
.navigationDestination(for: MediaItem.self) { MediaRouter(item: $0) }
}
}
}
private func bootstrap() async {
loading = true
error = nil
do {
async let homeTask = app.api.home()
async let prefsTask = app.api.getShelfPrefs()
home = try await homeTask
shelfPrefs = (try? await prefsTask)?.sorted(by: { $0.sortOrder < $1.sortOrder }) ?? []
if case .catalog(let id) = mode {
await loadCatalog(id, reset: true)
}
loading = false
} catch {
self.error = error.localizedDescription
loading = false
}
}
private func activate(_ mode: BrowseMode) async {
switch mode {
case .planks:
break
case .catalog(let id):
await loadCatalog(id, reset: true)
}
}
private func loadCatalog(_ id: String, reset: Bool) async {
if reset {
items = []
loading = true
}
do {
let page = try await app.api.browse(catalogId: id, skip: 0, take: 60)
total = page.total ?? page.items.count
items = page.items
loading = false
} catch {
self.error = error.localizedDescription
loading = false
}
}
private func loadMore() async {
guard case .catalog(let id) = mode, !loadingMore, items.count < total else { return }
loadingMore = true
do {
let page = try await app.api.browse(catalogId: id, skip: items.count, take: 60)
items.append(contentsOf: page.items)
loadingMore = false
} catch {
loadingMore = false
}
}
}