import SwiftUI struct SettingsView: View { @EnvironmentObject private var app: AppModel @State private var prefs: [ShelfPref] = [] @State private var prefsError: String? @State private var prefsMessage: String? @State private var savingPrefs = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 36) { VStack(alignment: .leading, spacing: 8) { Text("Instellingen") .font(TvFonts.title) .foregroundStyle(TvColors.text) Text("Profiel, home-planken en account.") .font(TvFonts.body) .foregroundStyle(TvColors.textDim) } profileHero settingsSection(title: "Profiel", subtitle: "Wissel of beheer kijkprofielen") { HStack(spacing: 20) { settingsActionButton( title: "Profiel wisselen", systemImage: "person.2.fill" ) { Task { await app.switchProfile() } } NavigationLink { ManageProfilesView() } label: { settingsActionLabel( title: app.canManageProfiles ? "Profielen beheren" : "Profielen bekijken", systemImage: "person.crop.circle.badge.plus" ) } .buttonStyle(FocusableCardStyle(scale: 1.04)) } .focusSection() } settingsSection( title: "Home-planken", subtitle: "Zet planken aan/uit en bepaal de volgorde op Home" ) { if prefs.isEmpty { Text(prefsError ?? "Geen planken geladen") .font(TvFonts.caption) .foregroundStyle(TvColors.textDim) } else { VStack(spacing: 12) { ForEach(Array(prefs.enumerated()), id: \.element.id) { index, pref in ShelfPrefRow( pref: pref, index: index, total: prefs.count, onToggle: { prefs[index].visible.toggle() Task { await persistPrefs() } }, onMoveUp: { movePref(from: index, to: index - 1) }, onMoveDown: { movePref(from: index, to: index + 1) } ) } } .focusSection() HStack(spacing: 16) { if savingPrefs { ProgressView().tint(.white).scaleEffect(0.8) } if let prefsMessage { Label(prefsMessage, systemImage: "checkmark.circle.fill") .font(TvFonts.caption) .foregroundStyle(TvColors.success) } if let prefsError { Text(prefsError) .font(TvFonts.caption) .foregroundStyle(TvColors.danger) } } .padding(.top, 4) } } settingsSection(title: "Account", subtitle: "Sessie op dit apparaat") { settingsActionButton( title: "Uitloggen", systemImage: "rectangle.portrait.and.arrow.right", destructive: true ) { Task { await app.logout() } } } settingsSection(title: "Over deze app", subtitle: nil) { VStack(spacing: 0) { infoRow("App", "Vonas Media Cluster ยท Apple TV") divider infoRow("Versie", "1.0.0") divider infoRow("Platform", "apple_tv") divider infoRow("Live TV", app.iptvEnabled ? "Aan" : "Uit") divider infoRow("Apparaat", AppModel.deviceName) divider infoRow("Server", app.store.apiBase) } } } .padding(.horizontal, 56) .padding(.top, 8) .padding(.bottom, 72) } .background( ZStack { TvColors.bg LinearGradient( colors: [TvColors.accent.opacity(0.12), .clear, .clear], startPoint: .topLeading, endPoint: .bottomTrailing ) } .ignoresSafeArea() ) .task { await loadPrefs() if let payload = try? await app.api.listProfiles() { app.canManageProfiles = payload.canManage == true } } } private var profileHero: some View { HStack(spacing: 28) { Text(ProfileAvatars.emoji(for: app.store.profileAvatarId)) .font(.system(size: 64)) .frame(width: 112, height: 112) .background( LinearGradient( colors: [TvColors.surfaceSoft, TvColors.surface], startPoint: .topLeading, endPoint: .bottomTrailing ), in: Circle() ) .overlay(Circle().strokeBorder(TvColors.accent.opacity(0.45), lineWidth: 2)) VStack(alignment: .leading, spacing: 10) { Text(app.store.profileName ?? "Profiel") .font(TvFonts.headline) .foregroundStyle(TvColors.text) Text("Actief kijkprofiel") .font(TvFonts.caption) .foregroundStyle(TvColors.textDim) Text(app.store.apiBase) .font(.system(size: 18, weight: .medium, design: .monospaced)) .foregroundStyle(TvColors.textDim.opacity(0.9)) .lineLimit(1) } Spacer(minLength: 0) } .padding(32) .background( RoundedRectangle(cornerRadius: 24, style: .continuous) .fill(.ultraThinMaterial) .overlay( RoundedRectangle(cornerRadius: 24, style: .continuous) .strokeBorder( LinearGradient( colors: [TvColors.accent.opacity(0.45), Color.white.opacity(0.08)], startPoint: .topLeading, endPoint: .bottomTrailing ), lineWidth: 1.5 ) ) ) } private func settingsSection( title: String, subtitle: String?, @ViewBuilder content: () -> Content ) -> some View { VStack(alignment: .leading, spacing: 18) { VStack(alignment: .leading, spacing: 6) { Text(title) .font(TvFonts.headline) .foregroundStyle(TvColors.text) if let subtitle { Text(subtitle) .font(TvFonts.caption) .foregroundStyle(TvColors.textDim) } } content() .padding(24) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 22, style: .continuous) .fill(TvColors.bgElevated.opacity(0.92)) .overlay( RoundedRectangle(cornerRadius: 22, style: .continuous) .strokeBorder(Color.white.opacity(0.07), lineWidth: 1) ) ) } } private func settingsActionButton( title: String, systemImage: String, destructive: Bool = false, action: @escaping () -> Void ) -> some View { Button(action: action) { settingsActionLabel(title: title, systemImage: systemImage, destructive: destructive) } .buttonStyle(FocusableCardStyle(scale: 1.04)) } private func settingsActionLabel( title: String, systemImage: String, destructive: Bool = false ) -> some View { HStack(spacing: 14) { Image(systemName: systemImage) .font(.system(size: 24, weight: .semibold)) .foregroundStyle(destructive ? TvColors.danger : TvColors.accent) .frame(width: 36) Text(title) .font(TvFonts.body) .foregroundStyle(TvColors.text) Spacer(minLength: 8) Image(systemName: "chevron.right") .font(.system(size: 18, weight: .semibold)) .foregroundStyle(TvColors.textDim) } .padding(.horizontal, 22) .padding(.vertical, 18) .frame(minWidth: 320) .background(TvColors.surface, in: RoundedRectangle(cornerRadius: 16, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.08), lineWidth: 1) ) } private var divider: some View { Rectangle() .fill(Color.white.opacity(0.06)) .frame(height: 1) .padding(.vertical, 4) } private func infoRow(_ label: String, _ value: String) -> some View { HStack(alignment: .firstTextBaseline) { Text(label) .font(TvFonts.body) .foregroundStyle(TvColors.textDim) .frame(width: 160, alignment: .leading) Text(value) .font(TvFonts.body) .foregroundStyle(TvColors.text) .frame(maxWidth: .infinity, alignment: .trailing) .multilineTextAlignment(.trailing) } .padding(.vertical, 10) } private func movePref(from: Int, to: Int) { guard prefs.indices.contains(from), prefs.indices.contains(to) else { return } withAnimation(.easeOut(duration: 0.2)) { prefs.swapAt(from, to) } Task { await persistPrefs() } } private func loadPrefs() async { do { prefs = try await app.api.getShelfPrefs().sorted { $0.sortOrder < $1.sortOrder } prefsError = nil } catch { prefsError = error.localizedDescription } } private func persistPrefs() async { savingPrefs = true 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) prefsMessage = "Opgeslagen" prefsError = nil savingPrefs = false try? await Task.sleep(nanoseconds: 1_800_000_000) if prefsMessage == "Opgeslagen" { prefsMessage = nil } } catch { prefsError = error.localizedDescription prefsMessage = nil savingPrefs = false } } } private struct ShelfPrefRow: View { let pref: ShelfPref let index: Int let total: Int let onToggle: () -> Void let onMoveUp: () -> Void let onMoveDown: () -> Void var body: some View { HStack(spacing: 18) { Text("\(index + 1)") .font(.system(size: 22, weight: .bold, design: .rounded)) .foregroundStyle(TvColors.textDim) .frame(width: 36) VStack(alignment: .leading, spacing: 4) { Text(pref.name) .font(TvFonts.body) .foregroundStyle(TvColors.text) Text(pref.visible ? "Zichtbaar op Home" : "Verborgen") .font(TvFonts.caption) .foregroundStyle(pref.visible ? TvColors.success.opacity(0.9) : TvColors.textDim) } Spacer(minLength: 12) HStack(spacing: 10) { Button(action: onMoveUp) { Image(systemName: "chevron.up") .font(.system(size: 20, weight: .bold)) .frame(width: 52, height: 52) .background(TvColors.surfaceSoft, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) } .buttonStyle(FocusableCardStyle(scale: 1.08)) .disabled(index == 0) .opacity(index == 0 ? 0.35 : 1) Button(action: onMoveDown) { Image(systemName: "chevron.down") .font(.system(size: 20, weight: .bold)) .frame(width: 52, height: 52) .background(TvColors.surfaceSoft, in: RoundedRectangle(cornerRadius: 12, style: .continuous)) } .buttonStyle(FocusableCardStyle(scale: 1.08)) .disabled(index >= total - 1) .opacity(index >= total - 1 ? 0.35 : 1) Button(action: onToggle) { Text(pref.visible ? "Aan" : "Uit") .font(.system(size: 20, weight: .semibold, design: .rounded)) .foregroundStyle(pref.visible ? .white : TvColors.textDim) .frame(width: 88, height: 52) .background( pref.visible ? TvColors.accent : TvColors.surfaceSoft, in: RoundedRectangle(cornerRadius: 12, style: .continuous) ) } .buttonStyle(FocusableCardStyle(scale: 1.08)) } } .padding(.horizontal, 18) .padding(.vertical, 14) .background( RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(TvColors.surface.opacity(0.85)) .overlay( RoundedRectangle(cornerRadius: 16, style: .continuous) .strokeBorder(Color.white.opacity(0.06), lineWidth: 1) ) ) } }