diff --git a/nahbar/nahbar/SettingsView.swift b/nahbar/nahbar/SettingsView.swift index a390271..81e9dd0 100644 --- a/nahbar/nahbar/SettingsView.swift +++ b/nahbar/nahbar/SettingsView.swift @@ -17,26 +17,17 @@ struct SettingsView: View { @AppStorage("aftermathDelayOption") private var aftermathDelayRaw: String = AftermathDelayOption.hours36.rawValue @AppStorage("icloudSyncEnabled") private var icloudSyncEnabled: Bool = false @State private var icloudToggleChanged = false - @AppStorage("aiAPIKey") private var aiAPIKey: String = AIConfig.fallback.apiKey - @AppStorage("aiModel") private var aiModel: String = AIConfig.fallback.model + @AppStorage("aiAPIKey") private var aiAPIKey: String = AIConfig.fallback.apiKey + @AppStorage("aiModel") private var aiModel: String = AIConfig.fallback.model @StateObject private var store = StoreManager.shared @StateObject private var personalityStore = PersonalityStore.shared - @Environment(\.modelContext) private var modelContext - @Environment(TourCoordinator.self) private var tourCoordinator @State private var showingPINSetup = false @State private var showingPINDisable = false @State private var showPaywall = false - @State private var showingResetConfirmation = false @State private var showingQuiz = false @AppStorage("hasSkippedPersonalityQuiz") private var hasSkippedPersonalityQuiz = false - // Onboarding-Flags zum Zurücksetzen - @AppStorage("nahbarOnboardingDone") private var nahbarOnboardingDone = false - @AppStorage("callWindowOnboardingDone") private var callWindowOnboardingDone = false - @AppStorage("photoRepairPassDone") private var photoRepairPassDone = false - @AppStorage("callSuggestionDate") private var callSuggestionDate = "" - private var biometricLabel: String { switch appLockManager.biometricType { case .faceID: return String(localized: "Face ID aktiviert") @@ -61,599 +52,490 @@ struct SettingsView: View { .padding(.horizontal, 20) .padding(.top, 12) - // Abonnement (oben) - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Abonnement", icon: "star.fill") - .padding(.horizontal, 20) + abonnementSection + darstellungSection + funktionenSection + systemSection - if store.isMax { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Max aktiv") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Text("Alle Features freigeschaltet") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - Image(systemName: "checkmark.seal.fill") - .foregroundStyle(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } else { - Button { showPaywall = true } label: { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Pro oder Max-Abo") - .font(.system(size: 15, weight: .medium)) - .foregroundStyle(theme.accent) - Text(store.isPro - ? "Auf Max upgraden – KI-Analyse freischalten" - : "KI-Analyse, Themes & mehr") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - } - } - .sheet(isPresented: $showPaywall) { PaywallView(targeting: store.isPro ? .max : .pro) } - - // Theme picker - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Atmosphäre", icon: "paintpalette") - .padding(.horizontal, 20) - - NavigationLink(destination: ThemePickerView()) { - HStack(spacing: 14) { - // Swatch - ZStack(alignment: .bottomTrailing) { - RoundedRectangle(cornerRadius: 8) - .fill(NahbarTheme.theme(for: activeThemeID).backgroundPrimary) - .frame(width: 40, height: 40) - .overlay( - RoundedRectangle(cornerRadius: 8) - .stroke(theme.borderSubtle, lineWidth: 1) - ) - RoundedRectangle(cornerRadius: 3) - .fill(NahbarTheme.theme(for: activeThemeID).accent) - .frame(width: 13, height: 13) - .padding(2) - } - VStack(alignment: .leading, spacing: 2) { - Text(activeThemeID.displayName) - .font(.system(size: 15, weight: .medium)) - .foregroundStyle(theme.contentPrimary) - Text(activeThemeID.tagline) - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - } - - // App-Schutz - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "App-Schutz", icon: "lock") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Code-Schutz") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - if appLockManager.isEnabled { - Text(biometricLabel) - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - } - Spacer() - Toggle("", isOn: Binding( - get: { appLockManager.isEnabled }, - set: { newValue in - if newValue { showingPINSetup = true } - else { showingPINDisable = true } - } - )) - .tint(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - - if appLockManager.isEnabled { - RowDivider() - Button { showingPINSetup = true } label: { - HStack { - Text("Code ändern") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - .sheet(isPresented: $showingPINSetup, onDismiss: { appLockManager.refreshBiometricType() }) { - AppLockSetupView(isDisabling: false) - .environmentObject(appLockManager) - } - .sheet(isPresented: $showingPINDisable) { - AppLockSetupView(isDisabling: true) - .environmentObject(appLockManager) - } - - // Gesprächszeit - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Gesprächszeit", icon: "phone.arrow.up.right") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - HStack { - Text("Aktiv") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Toggle("", isOn: $callWindowManager.isEnabled) - .tint(theme.accent) - .onChange(of: callWindowManager.isEnabled) { _, enabled in - if enabled { - callWindowManager.scheduleNotifications() - } else { - callWindowManager.cancelNotifications() - } - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - - if callWindowManager.isEnabled { - RowDivider() - NavigationLink { - CallWindowSetupView( - manager: callWindowManager, - isOnboarding: false, - onDone: {} - ) - } label: { - HStack { - Text("Zeitfenster") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Text(callWindowManager.windowDescription) - .font(.system(size: 14)) - .foregroundStyle(theme.contentTertiary) - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - - // Kalender-Einstellungen - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Kalender-Einstellungen", icon: "calendar") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - HStack { - Text("Vorschau Geburtstage & Termine") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Picker("", selection: $daysAhead) { - Text("3 Tage").tag(3) - Text("1 Woche").tag(7) - Text("2 Wochen").tag(14) - Text("1 Monat").tag(30) - } - .tint(theme.accent) - } - .padding(.horizontal, 16) + // Entwickler (versteckt, nur für Entwickler) + NavigationLink(destination: DeveloperSettingsView()) { + Text("Entwickler") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + .frame(maxWidth: .infinity) .padding(.vertical, 8) - - if settingsCalendars.count > 1 { - RowDivider() - HStack { - Text("Kalender") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Picker("", selection: $defaultCalendarID) { - ForEach(settingsCalendars, id: \.calendarIdentifier) { cal in - HStack { - Image(systemName: "circle.fill") - .foregroundStyle(Color(cal.cgColor)) - Text(cal.title) - } - .tag(cal.calendarIdentifier) - } - } - .tint(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 8) - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - .task { - guard settingsCalendars.isEmpty else { return } - let calendars = await CalendarManager.shared.availableCalendars() - settingsCalendars = calendars - if defaultCalendarID.isEmpty || !calendars.map(\.calendarIdentifier).contains(defaultCalendarID) { - defaultCalendarID = CalendarManager.shared.defaultCalendarIdentifier ?? "" - } - } - } - - // Treffen & Bewertungen - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Treffen", icon: "star.fill") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("Nachwirkungs-Erinnerung") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Text("Push-Benachrichtigung nach dem Besuch") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - Toggle("", isOn: $aftermathNotificationsEnabled) - .tint(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - - if aftermathNotificationsEnabled { - RowDivider() - HStack { - Text("Verzögerung") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Spacer() - Picker("", selection: $aftermathDelayRaw) { - ForEach(AftermathDelayOption.allCases, id: \.rawValue) { opt in - Text(opt.label).tag(opt.rawValue) - } - } - .tint(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 8) - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - - // KI-Einstellungen - VStack(alignment: .leading, spacing: 12) { - HStack(spacing: 8) { - SectionHeader(title: "KI-Analyse", icon: "sparkles") - MaxBadge() - } - .padding(.horizontal, 20) - - VStack(spacing: 0) { - settingsTextField(label: "Modell", value: $aiModel, placeholder: AIConfig.fallback.model) - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - - // iCloud - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "iCloud", icon: "icloud") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - // Toggle - HStack { - VStack(alignment: .leading, spacing: 2) { - Text("iCloud-Sync") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Text(icloudSyncEnabled - ? "Daten werden geräteübergreifend synchronisiert" - : "Daten werden nur lokal gespeichert") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - Toggle("", isOn: Binding( - get: { icloudSyncEnabled }, - set: { newValue in - icloudSyncEnabled = newValue - icloudToggleChanged = true - } - )) - .tint(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - - // Live-Sync-Status (nur wenn aktiviert) - if icloudSyncEnabled { - RowDivider() - HStack(spacing: 8) { - Image(systemName: cloudSyncMonitor.state.systemImage) - .font(.system(size: 12)) - .foregroundStyle(cloudSyncMonitor.state.isError ? .red : theme.contentTertiary) - .symbolEffect(.pulse, isActive: cloudSyncMonitor.state == .syncing) - Text(cloudSyncMonitor.state.statusText) - .font(.system(size: 12)) - .foregroundStyle(cloudSyncMonitor.state.isError ? .red : theme.contentTertiary) - Spacer() - } - .padding(.horizontal, 16) - .padding(.vertical, 10) - } - - // Neustart-Banner wenn Toggle verändert wurde - if icloudToggleChanged { - RowDivider() - HStack(spacing: 10) { - Image(systemName: "arrow.clockwise.circle.fill") - .font(.system(size: 14)) - .foregroundStyle(theme.accent) - Text("Neustart erforderlich, um die Änderung zu übernehmen.") - .font(.system(size: 12)) - .foregroundStyle(theme.contentSecondary) - Spacer() - Button("Jetzt") { - exit(0) - } - .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(theme.accent) - } - .padding(.horizontal, 16) - .padding(.vertical, 10) - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - .animation(.easeInOut(duration: 0.2), value: icloudSyncEnabled) - .animation(.easeInOut(duration: 0.2), value: icloudToggleChanged) - .animation(.easeInOut(duration: 0.2), value: cloudSyncMonitor.state == .syncing) - } - - // Persönlichkeit - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Persönlichkeit", icon: "brain") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - if let profile = personalityStore.profile, profile.isComplete { - // Empfohlenes Intervall - let days = PersonalityEngine.suggestedNudgeInterval(for: profile) - HStack(spacing: 8) { - VStack(alignment: .leading, spacing: 2) { - Text("Empfohlenes Nudge-Intervall") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Text("Alle \(days) Tage – basierend auf deinem Profil") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - RecommendedBadge(variant: .small) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - - RowDivider() - - // Quiz zurücksetzen - Button { - personalityStore.reset() - hasSkippedPersonalityQuiz = false - } label: { - HStack { - Text("Quiz zurücksetzen") - .font(.system(size: 15)) - .foregroundStyle(theme.contentSecondary) - Spacer() - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - } else { - Button { - hasSkippedPersonalityQuiz = false - showingQuiz = true - } label: { - HStack { - Text("Persönlichkeitsquiz starten") - .font(.system(size: 15)) - .foregroundStyle(theme.accent) - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - } - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - } - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - .sheet(isPresented: $showingQuiz) { - PersonalityQuizView { _ in } - } - - // Diagnose / Entwickler-Tools - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Diagnose", icon: "list.bullet.rectangle") - .padding(.horizontal, 20) - - // App zurücksetzen - Button { - showingResetConfirmation = true - } label: { - HStack(spacing: 14) { - Image(systemName: "arrow.counterclockwise") - .font(.system(size: 15)) - .foregroundStyle(.red) - .frame(width: 22) - VStack(alignment: .leading, spacing: 2) { - Text("App zurücksetzen") - .font(.system(size: 15)) - .foregroundStyle(.red) - Text("Onboarding, Profil und alle Daten löschen") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - - NavigationLink(destination: LogExportView()) { - HStack(spacing: 14) { - Image(systemName: "doc.text") - .font(.system(size: 15)) - .foregroundStyle(theme.contentTertiary) - .frame(width: 22) - VStack(alignment: .leading, spacing: 2) { - Text("Entwickler-Log") - .font(.system(size: 15)) - .foregroundStyle(theme.contentPrimary) - Text("\(AppEventLog.shared.entries.count) Einträge – Export als Textdatei") - .font(.system(size: 12)) - .foregroundStyle(theme.contentTertiary) - } - Spacer() - Image(systemName: "chevron.right") - .font(.system(size: 12, weight: .medium)) - .foregroundStyle(theme.contentTertiary) - } - .padding(.horizontal, 16) - .padding(.vertical, 12) - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) - } - } - - // App-Touren (deaktiviert) - - // About - VStack(alignment: .leading, spacing: 12) { - SectionHeader(title: "Über nahbar", icon: "info.circle") - .padding(.horizontal, 20) - - VStack(spacing: 0) { - SettingsInfoRow(label: "Version", value: "1.0 Draft") - RowDivider() - SettingsInfoRow(label: "Datenschutz", value: "Deine Daten verlassen nicht dein Gerät") - } - .background(theme.surfaceCard) - .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) - .padding(.horizontal, 20) } + .padding(.horizontal, 20) } .padding(.bottom, 40) } .background(theme.backgroundPrimary.ignoresSafeArea()) .navigationBarHidden(true) } - .confirmationDialog( - "App wirklich zurücksetzen?", - isPresented: $showingResetConfirmation, - titleVisibility: .visible - ) { - Button("Alles löschen und Onboarding starten", role: .destructive) { - resetApp() - } - Button("Abbrechen", role: .cancel) {} - } message: { - Text("Alle Personen, Momente, Besuche und dein Profil werden unwiderruflich gelöscht. Die App startet neu.") + .sheet(isPresented: $showingPINSetup, onDismiss: { appLockManager.refreshBiometricType() }) { + AppLockSetupView(isDisabling: false).environmentObject(appLockManager) + } + .sheet(isPresented: $showingPINDisable) { + AppLockSetupView(isDisabling: true).environmentObject(appLockManager) + } + .sheet(isPresented: $showPaywall) { + PaywallView(targeting: store.isPro ? .max : .pro) + } + .sheet(isPresented: $showingQuiz) { + PersonalityQuizView { _ in } } } - // MARK: - App Reset (Entwickler-Tool) + // MARK: - 1 · Abonnement (hervorgehoben) - private func resetApp() { - // 1. SwiftData: alle Objekte löschen - try? modelContext.delete(model: Person.self) - try? modelContext.delete(model: Moment.self) - try? modelContext.delete(model: LogEntry.self) - try? modelContext.delete(model: Visit.self) - try? modelContext.delete(model: Rating.self) - try? modelContext.delete(model: HealthSnapshot.self) - try? modelContext.delete(model: PersonPhoto.self) + private var abonnementSection: some View { + VStack(alignment: .leading, spacing: 12) { + SectionHeader(title: "Abonnement", icon: "star.fill") + .padding(.horizontal, 20) - // 2. Profil und Kontakte löschen - UserProfileStore.shared.reset() - ContactStore.shared.reset() + if store.isMax { + HStack(spacing: 14) { + Image(systemName: "checkmark.seal.fill") + .font(.system(size: 26)) + .foregroundStyle(theme.accent) + VStack(alignment: .leading, spacing: 3) { + Text("Max aktiv") + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(theme.contentPrimary) + Text("Alle Features freigeschaltet") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: theme.radiusCard) + .fill(theme.accent.opacity(0.07)) + .overlay( + RoundedRectangle(cornerRadius: theme.radiusCard) + .stroke(theme.accent.opacity(0.22), lineWidth: 1) + ) + ) + .padding(.horizontal, 20) + } else { + Button { showPaywall = true } label: { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(store.isPro ? "Auf Max upgraden" : "nahbar Pro oder Max") + .font(.system(size: 15, weight: .semibold)) + .foregroundStyle(theme.accent) + Text(store.isPro + ? "KI Insights freischalten" + : "KI Insights, Themes & mehr") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.accent.opacity(0.5)) + } + .padding(.horizontal, 16) + .padding(.vertical, 16) + .background( + RoundedRectangle(cornerRadius: theme.radiusCard) + .fill(theme.accent.opacity(0.07)) + .overlay( + RoundedRectangle(cornerRadius: theme.radiusCard) + .stroke(theme.accent.opacity(0.22), lineWidth: 1) + ) + ) + .padding(.horizontal, 20) + } + } + } + } - // 3. Onboarding- und Migrations-Flags zurücksetzen - nahbarOnboardingDone = false - callWindowOnboardingDone = false - photoRepairPassDone = false - callSuggestionDate = "" - UserDefaults.standard.removeObject(forKey: "visitMigrationPassDone") - UserDefaults.standard.removeObject(forKey: "nextStepMigrationPassDone") - tourCoordinator.resetSeenTours() + // MARK: - 2 · Darstellung & Profil - // 4. App neu starten damit alle States frisch initialisiert werden - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { exit(0) } + private var darstellungSection: some View { + VStack(alignment: .leading, spacing: 12) { + SectionHeader(title: "Darstellung & Profil", icon: "paintpalette") + .padding(.horizontal, 20) + + VStack(spacing: 0) { + // Theme + NavigationLink(destination: ThemePickerView()) { + HStack(spacing: 14) { + ZStack(alignment: .bottomTrailing) { + RoundedRectangle(cornerRadius: 8) + .fill(NahbarTheme.theme(for: activeThemeID).backgroundPrimary) + .frame(width: 40, height: 40) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(theme.borderSubtle, lineWidth: 1) + ) + RoundedRectangle(cornerRadius: 3) + .fill(NahbarTheme.theme(for: activeThemeID).accent) + .frame(width: 13, height: 13) + .padding(2) + } + VStack(alignment: .leading, spacing: 2) { + Text(activeThemeID.displayName) + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(theme.contentPrimary) + Text(activeThemeID.tagline) + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + + RowDivider() + + // Persönlichkeit + if let profile = personalityStore.profile, profile.isComplete { + let days = PersonalityEngine.suggestedNudgeInterval(for: profile) + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Persönlichkeit") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Text("Nudge alle \(days) Tage · Quiz abgeschlossen") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Button { + personalityStore.reset() + hasSkippedPersonalityQuiz = false + } label: { + Text("Zurücksetzen") + .font(.system(size: 13)) + .foregroundStyle(theme.contentTertiary) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } else { + Button { + hasSkippedPersonalityQuiz = false + showingQuiz = true + } label: { + HStack { + Text("Persönlichkeitsquiz") + .font(.system(size: 15)) + .foregroundStyle(theme.accent) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + } + } + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .padding(.horizontal, 20) + } + } + + // MARK: - 3 · Funktionen + + private var funktionenSection: some View { + VStack(alignment: .leading, spacing: 12) { + SectionHeader(title: "Funktionen", icon: "slider.horizontal.3") + .padding(.horizontal, 20) + + VStack(spacing: 0) { + // Gesprächszeit + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Gesprächszeit") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Text("Tägliche Erinnerung für Anrufe") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Toggle("", isOn: $callWindowManager.isEnabled) + .tint(theme.accent) + .onChange(of: callWindowManager.isEnabled) { _, enabled in + if enabled { callWindowManager.scheduleNotifications() } + else { callWindowManager.cancelNotifications() } + } + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + if callWindowManager.isEnabled { + RowDivider() + NavigationLink { + CallWindowSetupView(manager: callWindowManager, isOnboarding: false, onDone: {}) + } label: { + HStack { + Text("Zeitfenster") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Text(callWindowManager.windowDescription) + .font(.system(size: 14)) + .foregroundStyle(theme.contentTertiary) + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + } + + RowDivider() + + // Kalender + HStack { + Text("Termine & Geburtstage") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Picker("", selection: $daysAhead) { + Text("3 Tage").tag(3) + Text("1 Woche").tag(7) + Text("2 Wochen").tag(14) + Text("1 Monat").tag(30) + } + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + + if settingsCalendars.count > 1 { + RowDivider() + HStack { + Text("Kalender") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Picker("", selection: $defaultCalendarID) { + ForEach(settingsCalendars, id: \.calendarIdentifier) { cal in + HStack { + Image(systemName: "circle.fill") + .foregroundStyle(Color(cal.cgColor)) + Text(cal.title) + } + .tag(cal.calendarIdentifier) + } + } + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + + RowDivider() + + // Treffen + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Nachwirkungs-Erinnerung") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Text("Push nach dem Treffen") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Toggle("", isOn: $aftermathNotificationsEnabled) + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + if aftermathNotificationsEnabled { + RowDivider() + HStack { + Text("Verzögerung") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Picker("", selection: $aftermathDelayRaw) { + ForEach(AftermathDelayOption.allCases, id: \.rawValue) { opt in + Text(opt.label).tag(opt.rawValue) + } + } + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + } + + RowDivider() + + // KI Modell + HStack { + HStack(spacing: 6) { + Text("KI Modell") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + MaxBadge() + } + Spacer() + TextField(AIConfig.fallback.model, text: $aiModel) + .font(.system(size: 14)) + .foregroundStyle(theme.contentSecondary) + .autocorrectionDisabled() + .textInputAutocapitalization(.never) + .multilineTextAlignment(.trailing) + .frame(maxWidth: 180) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .padding(.horizontal, 20) + .task { + guard settingsCalendars.isEmpty else { return } + let calendars = await CalendarManager.shared.availableCalendars() + settingsCalendars = calendars + if defaultCalendarID.isEmpty || !calendars.map(\.calendarIdentifier).contains(defaultCalendarID) { + defaultCalendarID = CalendarManager.shared.defaultCalendarIdentifier ?? "" + } + } + } + } + + // MARK: - 4 · System + + private var systemSection: some View { + VStack(alignment: .leading, spacing: 12) { + SectionHeader(title: "System", icon: "gear") + .padding(.horizontal, 20) + + VStack(spacing: 0) { + // App-Schutz + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("Code-Schutz") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + if appLockManager.isEnabled { + Text(biometricLabel) + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + } + Spacer() + Toggle("", isOn: Binding( + get: { appLockManager.isEnabled }, + set: { newValue in + if newValue { showingPINSetup = true } + else { showingPINDisable = true } + } + )) + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + if appLockManager.isEnabled { + RowDivider() + Button { showingPINSetup = true } label: { + HStack { + Text("Code ändern") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + } + + RowDivider() + + // iCloud + HStack { + VStack(alignment: .leading, spacing: 2) { + Text("iCloud-Sync") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Text(icloudSyncEnabled + ? "Geräteübergreifend synchronisiert" + : "Nur lokal gespeichert") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Toggle("", isOn: Binding( + get: { icloudSyncEnabled }, + set: { newValue in + icloudSyncEnabled = newValue + icloudToggleChanged = true + } + )) + .tint(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + + if icloudSyncEnabled { + RowDivider() + HStack(spacing: 8) { + Image(systemName: cloudSyncMonitor.state.systemImage) + .font(.system(size: 12)) + .foregroundStyle(cloudSyncMonitor.state.isError ? .red : theme.contentTertiary) + .symbolEffect(.pulse, isActive: cloudSyncMonitor.state == .syncing) + Text(cloudSyncMonitor.state.statusText) + .font(.system(size: 12)) + .foregroundStyle(cloudSyncMonitor.state.isError ? .red : theme.contentTertiary) + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + } + + if icloudToggleChanged { + RowDivider() + HStack(spacing: 10) { + Image(systemName: "arrow.clockwise.circle.fill") + .font(.system(size: 14)) + .foregroundStyle(theme.accent) + Text("Neustart erforderlich, um die Änderung zu übernehmen.") + .font(.system(size: 12)) + .foregroundStyle(theme.contentSecondary) + Spacer() + Button("Jetzt") { exit(0) } + .font(.system(size: 12, weight: .semibold)) + .foregroundStyle(theme.accent) + } + .padding(.horizontal, 16) + .padding(.vertical, 10) + } + + RowDivider() + + // Über nahbar + SettingsInfoRow(label: "Version", value: "1.0 Draft") + RowDivider() + SettingsInfoRow(label: "Datenschutz", value: "Deine Daten verlassen nicht dein Gerät") + } + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .padding(.horizontal, 20) + .animation(.easeInOut(duration: 0.2), value: icloudSyncEnabled) + .animation(.easeInOut(duration: 0.2), value: icloudToggleChanged) + .animation(.easeInOut(duration: 0.2), value: cloudSyncMonitor.state == .syncing) + } } } @@ -678,6 +560,121 @@ extension SettingsView { } } +// MARK: - Developer Settings View + +private struct DeveloperSettingsView: View { + @Environment(\.nahbarTheme) var theme + @Environment(\.modelContext) private var modelContext + @Environment(TourCoordinator.self) private var tourCoordinator + @StateObject private var personalityStore = PersonalityStore.shared + + @AppStorage("nahbarOnboardingDone") private var nahbarOnboardingDone = false + @AppStorage("callWindowOnboardingDone") private var callWindowOnboardingDone = false + @AppStorage("photoRepairPassDone") private var photoRepairPassDone = false + @AppStorage("callSuggestionDate") private var callSuggestionDate = "" + @AppStorage("hasSkippedPersonalityQuiz") private var hasSkippedPersonalityQuiz = false + + @State private var showingResetConfirmation = false + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + VStack(spacing: 0) { + // App zurücksetzen + Button { showingResetConfirmation = true } label: { + HStack(spacing: 14) { + Image(systemName: "arrow.counterclockwise") + .font(.system(size: 15)) + .foregroundStyle(.red) + .frame(width: 22) + VStack(alignment: .leading, spacing: 2) { + Text("App zurücksetzen") + .font(.system(size: 15)) + .foregroundStyle(.red) + Text("Onboarding, Profil und alle Daten löschen") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + + RowDivider() + + // Entwickler-Log + NavigationLink(destination: LogExportView()) { + HStack(spacing: 14) { + Image(systemName: "doc.text") + .font(.system(size: 15)) + .foregroundStyle(theme.contentTertiary) + .frame(width: 22) + VStack(alignment: .leading, spacing: 2) { + Text("Entwickler-Log") + .font(.system(size: 15)) + .foregroundStyle(theme.contentPrimary) + Text("\(AppEventLog.shared.entries.count) Einträge – Export als Textdatei") + .font(.system(size: 12)) + .foregroundStyle(theme.contentTertiary) + } + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 12, weight: .medium)) + .foregroundStyle(theme.contentTertiary) + } + .padding(.horizontal, 16) + .padding(.vertical, 12) + } + } + .background(theme.surfaceCard) + .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) + .padding(.horizontal, 20) + } + .padding(.top, 16) + .padding(.bottom, 40) + } + .background(theme.backgroundPrimary.ignoresSafeArea()) + .navigationTitle("Entwickler") + .navigationBarTitleDisplayMode(.inline) + .themedNavBar() + .confirmationDialog( + "App wirklich zurücksetzen?", + isPresented: $showingResetConfirmation, + titleVisibility: .visible + ) { + Button("Alles löschen und Onboarding starten", role: .destructive) { resetApp() } + Button("Abbrechen", role: .cancel) {} + } message: { + Text("Alle Personen, Momente, Besuche und dein Profil werden unwiderruflich gelöscht. Die App startet neu.") + } + } + + private func resetApp() { + try? modelContext.delete(model: Person.self) + try? modelContext.delete(model: Moment.self) + try? modelContext.delete(model: LogEntry.self) + try? modelContext.delete(model: Visit.self) + try? modelContext.delete(model: Rating.self) + try? modelContext.delete(model: HealthSnapshot.self) + try? modelContext.delete(model: PersonPhoto.self) + + UserProfileStore.shared.reset() + ContactStore.shared.reset() + + nahbarOnboardingDone = false + callWindowOnboardingDone = false + photoRepairPassDone = false + callSuggestionDate = "" + hasSkippedPersonalityQuiz = false + UserDefaults.standard.removeObject(forKey: "visitMigrationPassDone") + UserDefaults.standard.removeObject(forKey: "nextStepMigrationPassDone") + tourCoordinator.resetSeenTours() + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { exit(0) } + } +} + // MARK: - Theme Option Row struct ThemeOptionRow: View { @@ -817,11 +814,11 @@ enum AppLanguage: String, CaseIterable { } } - var momentsLabel: String { self == .english ? "Moments" : "Momente" } - var logEntriesLabel: String { self == .english ? "Log entries" : "Log-Einträge" } - var birthYearLabel: String { self == .english ? "Birth year" : "Geburtsjahr" } - var interestsLabel: String { self == .english ? "Interests" : "Interessen" } - var culturalBackgroundLabel: String { self == .english ? "Cultural background": "Kultureller Hintergrund" } + var momentsLabel: String { self == .english ? "Moments" : "Momente" } + var logEntriesLabel: String { self == .english ? "Log entries" : "Log-Einträge" } + var birthYearLabel: String { self == .english ? "Birth year" : "Geburtsjahr" } + var interestsLabel: String { self == .english ? "Interests" : "Interessen" } + var culturalBackgroundLabel: String { self == .english ? "Cultural background" : "Kultureller Hintergrund" } /// Leitet die KI-Antwortsprache aus der iOS-Systemsprache ab. /// Unterstützte Sprachen: de, en – alle anderen fallen auf .german zurück.