import SwiftUI struct SettingsScreen: View { @EnvironmentObject private var app: AppModel var body: some View { List { Section { NavigationLink { EditProfileScreen(profileId: app.store.profileId) } label: { HStack(spacing: 14) { Text(ProfileAvatars.emoji(for: app.store.profileAvatarId)) .font(.system(size: 40)) .frame(width: 64, height: 64) .background( LinearGradient(colors: [McColors.surfaceSoft, McColors.surface], startPoint: .top, endPoint: .bottom), in: Circle() ) VStack(alignment: .leading, spacing: 4) { Text(app.store.profileName ?? "Profiel") .font(McFonts.headline) .foregroundStyle(McColors.text) Text("Naam & avatar wijzigen") .font(McFonts.caption) .foregroundStyle(McColors.textDim) } } } .listRowBackground(McColors.bgElevated) } Section("Bibliotheek") { NavigationLink { ShelfPrefsScreen() } label: { Label("Planken beheren", systemImage: "rectangle.stack") .foregroundStyle(McColors.text) } .listRowBackground(McColors.bgElevated) } Section("Profielen") { Button { Task { await app.switchProfile() } } label: { Label("Profiel wisselen", systemImage: "person.2") .foregroundStyle(McColors.text) } .listRowBackground(McColors.bgElevated) if app.canManageProfiles { NavigationLink { ManageProfilesScreen() } label: { Label("Profielen beheren", systemImage: "person.crop.circle.badge.plus") .foregroundStyle(McColors.text) } .listRowBackground(McColors.bgElevated) } } Section("Server") { LabeledContent("URL", value: app.store.apiBase) Button { Task { await app.logout() } } label: { Label("Server / account wisselen", systemImage: "server.rack") .foregroundStyle(McColors.text) } .listRowBackground(McColors.bgElevated) } Section { Button(role: .destructive) { Task { await app.logout() } } label: { Label("Uitloggen", systemImage: "rectangle.portrait.and.arrow.right") } .listRowBackground(McColors.bgElevated) } Section("Info") { LabeledContent("App", value: "VMC iOS") LabeledContent("Versie", value: "0.4.3") LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit") } .listRowBackground(McColors.bgElevated) } .scrollContentBackground(.hidden) .background(McColors.bg.ignoresSafeArea()) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .principal) { BrandHeader(subtitle: "ACCOUNT") } } .toolbarBackground(McColors.bg, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) } } struct EditProfileScreen: View { @EnvironmentObject private var app: AppModel let profileId: String? @State private var name = "" @State private var avatarId = "fox" @State private var pin = "" @State private var clearPin = false @State private var message: String? @State private var error: String? @State private var saving = false private let cols = [GridItem(.adaptive(minimum: 64), spacing: 12)] var body: some View { Form { Section("Naam") { TextField("Naam", text: $name) .foregroundStyle(McColors.text) } .listRowBackground(McColors.bgElevated) Section("Avatar") { LazyVGrid(columns: cols, spacing: 12) { ForEach(ProfileAvatars.all, id: \.id) { avatar in Button { avatarId = avatar.id } label: { Text(avatar.emoji) .font(.system(size: 28)) .frame(width: 56, height: 56) .background( avatarId == avatar.id ? McColors.accent.opacity(0.25) : McColors.surface, in: Circle() ) .overlay( Circle().stroke(avatarId == avatar.id ? McColors.accent : Color.clear, lineWidth: 2) ) } .buttonStyle(.plain) } } .padding(.vertical, 4) } .listRowBackground(McColors.bgElevated) Section("PIN (optioneel)") { SecureField("Nieuwe PIN (4 cijfers)", text: $pin) .keyboardType(.numberPad) .foregroundStyle(McColors.text) Toggle("PIN verwijderen", isOn: $clearPin) .tint(McColors.accent) } .listRowBackground(McColors.bgElevated) if let message { Section { Text(message).foregroundStyle(McColors.success) } .listRowBackground(McColors.bgElevated) } if let error { Section { Text(error).foregroundStyle(McColors.danger) } .listRowBackground(McColors.bgElevated) } Section { Button { Task { await save() } } label: { if saving { ProgressView() } else { Text("Opslaan") } } .disabled(saving || name.trimmingCharacters(in: .whitespaces).isEmpty) .listRowBackground(McColors.bgElevated) } } .scrollContentBackground(.hidden) .background(McColors.bg.ignoresSafeArea()) .navigationTitle("Mijn profiel") .navigationBarTitleDisplayMode(.inline) .task { name = app.store.profileName ?? "" avatarId = app.store.profileAvatarId ?? "fox" } } private func save() async { guard let profileId else { return } saving = true error = nil message = nil do { let updated = try await app.api.updateProfile( profileId: profileId, name: name.trimmingCharacters(in: .whitespacesAndNewlines), avatarId: avatarId, pin: clearPin ? nil : (pin.isEmpty ? nil : pin), clearPin: clearPin ) app.store.setProfile(id: updated.id, name: updated.name, avatarId: updated.avatarId) message = "Opgeslagen" pin = "" clearPin = false saving = false } catch { self.error = error.localizedDescription saving = false } } } struct ManageProfilesScreen: View { @EnvironmentObject private var app: AppModel @State private var profiles: [ViewerProfile] = [] @State private var canManage = false @State private var error: String? @State private var showCreate = false var body: some View { List { ForEach(profiles) { profile in NavigationLink { AdminEditProfileScreen(profile: profile, canManage: canManage) } label: { HStack(spacing: 12) { Text(ProfileAvatars.emoji(for: profile.avatarId)).font(.title2) VStack(alignment: .leading, spacing: 2) { Text(profile.name).foregroundStyle(McColors.text) Text(profile.isOwner == true ? "Hoofdprofiel" : (profile.isKids == true ? "Kids" : "Profiel")) .font(McFonts.caption) .foregroundStyle(McColors.textDim) } } } .listRowBackground(McColors.bgElevated) } if canManage && profiles.count < 8 { Button { showCreate = true } label: { Label("Profiel toevoegen", systemImage: "plus.circle.fill") .foregroundStyle(McColors.accent) } .listRowBackground(McColors.bgElevated) } if let error { Text(error).foregroundStyle(McColors.danger) .listRowBackground(McColors.bgElevated) } } .scrollContentBackground(.hidden) .background(McColors.bg.ignoresSafeArea()) .navigationTitle("Profielen") .navigationBarTitleDisplayMode(.inline) .task { await load() } .sheet(isPresented: $showCreate) { CreateProfileSheet { showCreate = false Task { await load() } } .environmentObject(app) } } private func load() async { do { let payload = try await app.api.listProfiles() profiles = payload.profiles canManage = payload.canManage == true app.canManageProfiles = canManage } catch { self.error = error.localizedDescription } } } struct CreateProfileSheet: View { @EnvironmentObject private var app: AppModel let onDone: () -> Void @State private var name = "" @State private var avatarId = "fox" @State private var isKids = false @State private var pin = "" @State private var error: String? @State private var saving = false private let cols = [GridItem(.adaptive(minimum: 56), spacing: 10)] var body: some View { NavigationStack { Form { Section("Naam") { TextField("Naam", text: $name) } .listRowBackground(McColors.bgElevated) Section("Avatar") { LazyVGrid(columns: cols, spacing: 10) { ForEach(ProfileAvatars.all, id: \.id) { avatar in Button { avatarId = avatar.id } label: { Text(avatar.emoji) .font(.title2) .frame(width: 48, height: 48) .background(avatarId == avatar.id ? McColors.accent.opacity(0.25) : McColors.surface, in: Circle()) } .buttonStyle(.plain) } } } .listRowBackground(McColors.bgElevated) Section { Toggle("Kids-profiel", isOn: $isKids).tint(McColors.accent) SecureField("PIN (optioneel)", text: $pin).keyboardType(.numberPad) } .listRowBackground(McColors.bgElevated) if let error { Text(error).foregroundStyle(McColors.danger) .listRowBackground(McColors.bgElevated) } } .scrollContentBackground(.hidden) .background(McColors.bg.ignoresSafeArea()) .navigationTitle("Nieuw profiel") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Annuleer", action: onDone) } ToolbarItem(placement: .confirmationAction) { Button("Maak") { Task { await create() } } .disabled(saving || name.trimmingCharacters(in: .whitespaces).isEmpty) } } } } private func create() async { saving = true error = nil do { _ = try await app.api.createProfile( name: name.trimmingCharacters(in: .whitespacesAndNewlines), avatarId: avatarId, isKids: isKids, pin: pin.isEmpty ? nil : pin ) saving = false onDone() } catch { self.error = error.localizedDescription saving = false } } } struct AdminEditProfileScreen: View { @EnvironmentObject private var app: AppModel let profile: ViewerProfile let canManage: Bool @Environment(\.dismiss) private var dismiss @State private var name = "" @State private var avatarId = "fox" @State private var isKids = false @State private var pin = "" @State private var clearPin = false @State private var error: String? @State private var message: String? var body: some View { Form { Section("Naam") { TextField("Naam", text: $name) } .listRowBackground(McColors.bgElevated) Section("Avatar") { ScrollView(.horizontal, showsIndicators: false) { HStack { ForEach(ProfileAvatars.all, id: \.id) { avatar in Button { avatarId = avatar.id } label: { Text(avatar.emoji) .font(.title) .padding(8) .background(avatarId == avatar.id ? McColors.accent.opacity(0.25) : McColors.surface, in: Circle()) } .buttonStyle(.plain) } } } } .listRowBackground(McColors.bgElevated) if canManage && profile.isOwner != true { Section { Toggle("Kids-profiel", isOn: $isKids).tint(McColors.accent) } .listRowBackground(McColors.bgElevated) } Section("PIN") { SecureField("Nieuwe PIN", text: $pin).keyboardType(.numberPad) Toggle("PIN verwijderen", isOn: $clearPin).tint(McColors.accent) } .listRowBackground(McColors.bgElevated) Section { Button("Opslaan") { Task { await save() } } .listRowBackground(McColors.bgElevated) } if canManage && profile.isOwner != true { Section { Button("Verwijderen", role: .destructive) { Task { await remove() } } .listRowBackground(McColors.bgElevated) } } if let message { Text(message).foregroundStyle(McColors.success).listRowBackground(McColors.bgElevated) } if let error { Text(error).foregroundStyle(McColors.danger).listRowBackground(McColors.bgElevated) } } .scrollContentBackground(.hidden) .background(McColors.bg.ignoresSafeArea()) .navigationTitle(profile.name) .task { name = profile.name avatarId = profile.avatarId ?? "fox" isKids = profile.isKids == true } } private func save() async { do { let updated = try await app.api.updateProfile( profileId: profile.id, name: name.trimmingCharacters(in: .whitespacesAndNewlines), avatarId: avatarId, isKids: canManage && profile.isOwner != true ? isKids : nil, pin: clearPin ? nil : (pin.isEmpty ? nil : pin), clearPin: clearPin ) if updated.id == app.store.profileId { app.store.setProfile(id: updated.id, name: updated.name, avatarId: updated.avatarId) } message = "Opgeslagen" error = nil } catch { self.error = error.localizedDescription } } private func remove() async { do { try await app.api.deleteProfile(profileId: profile.id) dismiss() } catch { self.error = error.localizedDescription } } } struct ShelfPrefsScreen: View { @EnvironmentObject private var app: AppModel @State private var prefs: [ShelfPref] = [] @State private var error: String? @State private var message: String? var body: some View { List { Section { Text("Zet planken aan/uit en sleep om te sorteren. Dit bepaalt Home én Films/Series.") .font(McFonts.caption) .foregroundStyle(McColors.textDim) } .listRowBackground(McColors.bgElevated) Section("Planken") { ForEach($prefs) { $pref in Toggle(isOn: $pref.visible) { VStack(alignment: .leading, spacing: 2) { Text(pref.name).foregroundStyle(McColors.text) Text((pref.kind ?? "").uppercased()) .font(McFonts.caption) .foregroundStyle(McColors.textDim) } } .tint(McColors.accent) .listRowBackground(McColors.bgElevated) .onChange(of: pref.visible) { _, _ in Task { await persist() } } } .onMove { from, to in prefs.move(fromOffsets: from, toOffset: to) Task { await persist() } } } if let message { Text(message).foregroundStyle(McColors.success).listRowBackground(McColors.bgElevated) } if let error { Text(error).foregroundStyle(McColors.danger).listRowBackground(McColors.bgElevated) } } .environment(\.editMode, .constant(.active)) .scrollContentBackground(.hidden) .background(McColors.bg.ignoresSafeArea()) .navigationTitle("Planken") .navigationBarTitleDisplayMode(.inline) .task { await load() } } private func load() async { do { prefs = try await app.api.getShelfPrefs().sorted { $0.sortOrder < $1.sortOrder } } catch { self.error = error.localizedDescription } } private func persist() async { do { let next = prefs.enumerated().map { i, p in ShelfPref( shelfId: p.shelfId, name: p.name, slug: p.slug, kind: p.kind, visible: p.visible, sortOrder: i ) } prefs = next try await app.api.saveShelfPrefs(next) message = "Opgeslagen" error = nil } catch { self.error = error.localizedDescription } } }