stremio/apps/ios/VMC/Data/AppModel.swift
Jos Vooges | STH 303383ea4e Add server URL choice, email login, and login lock controls.
Clients can use VMC or a custom server with email/password or device code; API locks after 3 failed logins with admin unlock.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 01:32:35 +02:00

150 lines
4.6 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, 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 = .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)
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)
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 ? "iPhone" : UIDevice.current.name
}
}