stremio/apps/ios/VMC/UI/ProfilesScreen.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

233 lines
8.1 KiB
Swift

import SwiftUI
struct ProfilesScreen: View {
@EnvironmentObject private var app: AppModel
@State private var profiles: [ViewerProfile] = []
@State private var error: String?
@State private var pinTarget: ViewerProfile?
@State private var pinDigits = ""
@State private var pinError: String?
@State private var loading = true
@State private var selecting = false
private let cols = [GridItem(.adaptive(minimum: 110), spacing: 20)]
var body: some View {
VStack(spacing: 28) {
Text("Wie kijkt er?")
.font(McFonts.title)
.foregroundStyle(McColors.text)
.padding(.top, 48)
if loading {
ProgressView().tint(.white)
} else if let error, profiles.isEmpty {
Text(error).foregroundStyle(McColors.danger)
Button("Opnieuw") { Task { await load() } }
.buttonStyle(McPrimaryButtonStyle())
} else {
if let error {
Text(error)
.font(McFonts.caption)
.foregroundStyle(McColors.danger)
.padding(.horizontal)
}
LazyVGrid(columns: cols, spacing: 24) {
ForEach(profiles) { profile in
Button {
Task { await select(profile) }
} label: {
VStack(spacing: 10) {
Text(ProfileAvatars.emoji(for: profile.avatarId))
.font(.system(size: 48))
.frame(width: 96, height: 96)
.background(
LinearGradient(
colors: [McColors.surfaceSoft, McColors.surface],
startPoint: .topLeading,
endPoint: .bottomTrailing
),
in: Circle()
)
.overlay(Circle().stroke(McColors.accent.opacity(0.25), lineWidth: 1))
Text(profile.name)
.font(McFonts.headline)
.foregroundStyle(McColors.text)
.lineLimit(1)
if profile.hasPin == true {
Image(systemName: "lock.fill")
.font(.caption2)
.foregroundStyle(McColors.textDim)
}
}
}
.buttonStyle(.plain)
.disabled(selecting)
}
}
.padding(.horizontal, 24)
}
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
LinearGradient(colors: [Color(red: 0.08, green: 0.03, blue: 0.04), McColors.bg], startPoint: .top, endPoint: .bottom)
.ignoresSafeArea()
)
.task { await load() }
.sheet(item: $pinTarget) { profile in
PinPadSheet(
profileName: profile.name,
avatarId: profile.avatarId,
digits: $pinDigits,
error: pinError,
busy: selecting,
onCancel: {
pinTarget = nil
pinDigits = ""
pinError = nil
},
onSubmit: { code in
Task { await submitPin(for: profile, code: code) }
}
)
.presentationDetents([.height(420)])
.presentationDragIndicator(.visible)
}
}
private func load() async {
loading = true
error = nil
do {
let payload = try await app.api.listProfiles()
profiles = payload.profiles
loading = false
} catch {
self.error = error.localizedDescription
loading = false
}
}
private func select(_ profile: ViewerProfile, pin: String? = nil) async {
if profile.hasPin == true, pin == nil {
pinDigits = ""
pinError = nil
pinTarget = profile
return
}
selecting = true
error = nil
do {
let selected = try await app.api.selectProfile(profileId: profile.id, pin: pin)
pinTarget = nil
pinDigits = ""
pinError = nil
selecting = false
app.didSelectProfile(selected)
} catch {
selecting = false
if pinTarget != nil {
pinError = error.localizedDescription
pinDigits = ""
} else {
self.error = error.localizedDescription
}
}
}
private func submitPin(for profile: ViewerProfile, code: String) async {
let digits = code.filter(\.isNumber)
guard digits.count == 4 else {
pinError = "PIN moet 4 cijfers zijn"
return
}
await select(profile, pin: String(digits))
}
}
struct PinPadSheet: View {
let profileName: String
let avatarId: String?
@Binding var digits: String
let error: String?
let busy: Bool
let onCancel: () -> Void
let onSubmit: (String) -> Void
private let keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "", "0", "⌫"]
var body: some View {
VStack(spacing: 18) {
HStack {
Button("Annuleer", action: onCancel)
.foregroundStyle(McColors.textDim)
Spacer()
}
.padding(.horizontal)
Text(ProfileAvatars.emoji(for: avatarId))
.font(.system(size: 40))
Text("PIN voor \(profileName)")
.font(McFonts.headline)
.foregroundStyle(McColors.text)
HStack(spacing: 12) {
ForEach(0..<4, id: \.self) { i in
Circle()
.fill(i < digits.count ? McColors.accent : McColors.surfaceSoft)
.frame(width: 14, height: 14)
}
}
.padding(.vertical, 4)
if let error {
Text(error)
.font(McFonts.caption)
.foregroundStyle(McColors.danger)
}
LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 12), count: 3), spacing: 12) {
ForEach(keys, id: \.self) { key in
if key.isEmpty {
Color.clear.frame(height: 52)
} else {
Button {
tap(key)
} label: {
Text(key)
.font(.system(size: 24, weight: .semibold, design: .rounded))
.foregroundStyle(McColors.text)
.frame(maxWidth: .infinity)
.frame(height: 52)
.background(McColors.surface, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
}
.buttonStyle(.plain)
.disabled(busy)
}
}
}
.padding(.horizontal, 28)
if busy {
ProgressView().tint(.white)
}
}
.padding(.top, 16)
.padding(.bottom, 24)
.background(McColors.bgElevated.ignoresSafeArea())
}
private func tap(_ key: String) {
if key == "⌫" {
if !digits.isEmpty { digits.removeLast() }
return
}
guard digits.count < 4 else { return }
digits.append(key)
if digits.count == 4 {
onSubmit(digits)
}
}
}