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.
163 lines
5 KiB
Swift
163 lines
5 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Combine
|
|
|
|
@MainActor
|
|
final class AppModel: ObservableObject {
|
|
enum Phase: Equatable {
|
|
case boot
|
|
case server
|
|
case auth
|
|
case password
|
|
case link
|
|
case profiles
|
|
case main
|
|
}
|
|
|
|
enum MainTab: String, CaseIterable, Identifiable {
|
|
case home, watchlist, movies, series, tv, search, settings
|
|
var id: String { rawValue }
|
|
|
|
var title: String {
|
|
switch self {
|
|
case .home: return "Home"
|
|
case .watchlist: return "Watchlist"
|
|
case .movies: return "Films"
|
|
case .series: return "Series"
|
|
case .tv: return "TV"
|
|
case .search: return "Zoeken"
|
|
case .settings: return "Instellingen"
|
|
}
|
|
}
|
|
|
|
var icon: String {
|
|
switch self {
|
|
case .home: return "house.fill"
|
|
case .watchlist: return "bookmark.fill"
|
|
case .movies: return "film"
|
|
case .series: return "rectangle.stack.fill"
|
|
case .tv: return "tv.fill"
|
|
case .search: return "magnifyingglass"
|
|
case .settings: return "gearshape.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?
|
|
@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 = .server
|
|
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,
|
|
showAvatar: profile.showsAvatar,
|
|
avatarUrl: profile.avatarUrl ?? ""
|
|
)
|
|
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 = .server
|
|
}
|
|
}
|
|
|
|
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,
|
|
showAvatar: profile.showsAvatar,
|
|
avatarUrl: profile.avatarUrl ?? ""
|
|
)
|
|
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 = .server
|
|
}
|
|
|
|
static var deviceName: String {
|
|
UIDevice.current.name.isEmpty ? "Apple TV" : UIDevice.current.name
|
|
}
|
|
}
|