SwiftUI app against master API: link, profiles, browse, live TV, favorites sync, and TestFlight-ready assets. Co-authored-by: Cursor <cursoragent@cursor.com>
147 lines
4.6 KiB
Swift
147 lines
4.6 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Combine
|
|
|
|
@MainActor
|
|
final class AppModel: ObservableObject {
|
|
enum Phase: Equatable {
|
|
case boot
|
|
case link
|
|
case profiles
|
|
case main
|
|
}
|
|
|
|
enum MainTab: String, CaseIterable, Identifiable {
|
|
case home, movies, series, tv, search, account
|
|
var id: String { rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .home: return "Home"
|
|
case .movies: return "Films"
|
|
case .series: return "Series"
|
|
case .tv: return "TV"
|
|
case .search: return "Zoeken"
|
|
case .account: return "Account"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .home: return "house.fill"
|
|
case .movies: return "film"
|
|
case .series: return "rectangle.stack.fill"
|
|
case .tv: return "tv.fill"
|
|
case .search: return "magnifyingglass"
|
|
case .account: return "person.crop.circle.fill"
|
|
}
|
|
}
|
|
}
|
|
|
|
let store = SessionStore()
|
|
let api = ApiClient()
|
|
let favorites = FavoritesStore()
|
|
|
|
@Published var phase: Phase = .boot
|
|
@Published var tab: MainTab = .home
|
|
@Published var iptvEnabled = false
|
|
@Published var canManageProfiles = false
|
|
@Published var bootError: String?
|
|
/// > 0 terwijl een speler (VOD/live) open is — verbergt de footer.
|
|
@Published private(set) var playbackDepth = 0
|
|
|
|
var isPlaybackActive: Bool { playbackDepth > 0 }
|
|
|
|
func enterPlayback() { playbackDepth += 1 }
|
|
func leavePlayback() { playbackDepth = max(0, playbackDepth - 1) }
|
|
|
|
private var cancellables = Set<AnyCancellable>()
|
|
|
|
init() {
|
|
favorites.objectWillChange
|
|
.receive(on: RunLoop.main)
|
|
.sink { [weak self] _ in self?.objectWillChange.send() }
|
|
.store(in: &cancellables)
|
|
Task { await bootstrap() }
|
|
}
|
|
|
|
func bootstrap() async {
|
|
phase = .boot
|
|
bootError = nil
|
|
await api.configure(baseUrl: store.apiBase, token: store.refreshToken)
|
|
guard store.isAuthed else {
|
|
phase = .link
|
|
return
|
|
}
|
|
do {
|
|
let me = try await api.clientMe()
|
|
iptvEnabled = me.iptvEnabled
|
|
if let profile = me.profile {
|
|
store.setProfile(id: profile.id, name: profile.name, avatarId: profile.avatarId)
|
|
favorites.bind(profileId: profile.id)
|
|
await favorites.refresh(using: api)
|
|
if let payload = try? await api.listProfiles() {
|
|
canManageProfiles = payload.canManage == true
|
|
} else {
|
|
canManageProfiles = profile.isOwner == true
|
|
}
|
|
phase = .main
|
|
} else {
|
|
store.setProfile(id: nil, name: nil, avatarId: nil)
|
|
favorites.bind(profileId: nil)
|
|
canManageProfiles = false
|
|
phase = .profiles
|
|
}
|
|
} catch {
|
|
bootError = error.localizedDescription
|
|
store.setToken(nil)
|
|
favorites.bind(profileId: nil)
|
|
await api.configure(baseUrl: store.apiBase, token: nil)
|
|
phase = .link
|
|
}
|
|
}
|
|
|
|
func didLink(token: String) async {
|
|
store.setToken(token)
|
|
await api.configure(baseUrl: store.apiBase, token: token)
|
|
store.setProfile(id: nil, name: nil, avatarId: nil)
|
|
if let me = try? await api.clientMe() {
|
|
iptvEnabled = me.iptvEnabled
|
|
}
|
|
phase = .profiles
|
|
}
|
|
|
|
func didSelectProfile(_ profile: ViewerProfile) {
|
|
store.setProfile(id: profile.id, name: profile.name, avatarId: profile.avatarId)
|
|
favorites.bind(profileId: profile.id)
|
|
Task {
|
|
await favorites.refresh(using: api)
|
|
if let payload = try? await api.listProfiles() {
|
|
canManageProfiles = payload.canManage == true
|
|
} else {
|
|
canManageProfiles = profile.isOwner == true
|
|
}
|
|
}
|
|
tab = .home
|
|
phase = .main
|
|
}
|
|
|
|
func switchProfile() async {
|
|
try? await api.clearProfile()
|
|
store.setProfile(id: nil, name: nil, avatarId: nil)
|
|
favorites.bind(profileId: nil)
|
|
phase = .profiles
|
|
}
|
|
|
|
func logout() async {
|
|
store.setToken(nil)
|
|
await api.configure(baseUrl: store.apiBase, token: nil)
|
|
favorites.bind(profileId: nil)
|
|
iptvEnabled = false
|
|
phase = .link
|
|
}
|
|
|
|
static var deviceName: String {
|
|
UIDevice.current.name.isEmpty ? "iPhone" : UIDevice.current.name
|
|
}
|
|
}
|