Fix #33: SettingsView in 4 klare Sektionen umstrukturiert

Vorher: 11 lose Sektionen ohne erkennbare Ordnung.
Nachher:
  1. Abonnement – visuell hervorgehoben (accent-getönte Karte mit Border)
  2. Darstellung & Profil – Theme + Persönlichkeitsquiz
  3. Funktionen – Gesprächszeit, Kalender, Treffen, KI-Modell
  4. System – App-Schutz, iCloud, Über nahbar
  + Entwickler-Link am Fuß → eigene SubView mit Reset & Dev-Log

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-23 12:47:58 +02:00
parent ace6801d01
commit 9e932f0f2c
+417 -420
View File
@@ -21,22 +21,13 @@ struct SettingsView: View {
@AppStorage("aiModel") private var aiModel: String = AIConfig.fallback.model @AppStorage("aiModel") private var aiModel: String = AIConfig.fallback.model
@StateObject private var store = StoreManager.shared @StateObject private var store = StoreManager.shared
@StateObject private var personalityStore = PersonalityStore.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 showingPINSetup = false
@State private var showingPINDisable = false @State private var showingPINDisable = false
@State private var showPaywall = false @State private var showPaywall = false
@State private var showingResetConfirmation = false
@State private var showingQuiz = false @State private var showingQuiz = false
@AppStorage("hasSkippedPersonalityQuiz") private var hasSkippedPersonalityQuiz = 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 { private var biometricLabel: String {
switch appLockManager.biometricType { switch appLockManager.biometricType {
case .faceID: return String(localized: "Face ID aktiviert") case .faceID: return String(localized: "Face ID aktiviert")
@@ -61,66 +52,118 @@ struct SettingsView: View {
.padding(.horizontal, 20) .padding(.horizontal, 20)
.padding(.top, 12) .padding(.top, 12)
// Abonnement (oben) abonnementSection
darstellungSection
funktionenSection
systemSection
// Entwickler (versteckt, nur für Entwickler)
NavigationLink(destination: DeveloperSettingsView()) {
Text("Entwickler")
.font(.system(size: 12))
.foregroundStyle(theme.contentTertiary)
.frame(maxWidth: .infinity)
.padding(.vertical, 8)
}
.padding(.horizontal, 20)
}
.padding(.bottom, 40)
}
.background(theme.backgroundPrimary.ignoresSafeArea())
.navigationBarHidden(true)
}
.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: - 1 · Abonnement (hervorgehoben)
private var abonnementSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "Abonnement", icon: "star.fill") SectionHeader(title: "Abonnement", icon: "star.fill")
.padding(.horizontal, 20) .padding(.horizontal, 20)
if store.isMax { if store.isMax {
HStack { HStack(spacing: 14) {
VStack(alignment: .leading, spacing: 2) { Image(systemName: "checkmark.seal.fill")
.font(.system(size: 26))
.foregroundStyle(theme.accent)
VStack(alignment: .leading, spacing: 3) {
Text("Max aktiv") Text("Max aktiv")
.font(.system(size: 15)) .font(.system(size: 15, weight: .semibold))
.foregroundStyle(theme.contentPrimary) .foregroundStyle(theme.contentPrimary)
Text("Alle Features freigeschaltet") Text("Alle Features freigeschaltet")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundStyle(theme.contentTertiary) .foregroundStyle(theme.contentTertiary)
} }
Spacer() Spacer()
Image(systemName: "checkmark.seal.fill")
.foregroundStyle(theme.accent)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 12) .padding(.vertical, 16)
.background(theme.surfaceCard) .background(
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) 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) .padding(.horizontal, 20)
} else { } else {
Button { showPaywall = true } label: { Button { showPaywall = true } label: {
HStack { HStack {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 4) {
Text("Pro oder Max-Abo") Text(store.isPro ? "Auf Max upgraden" : "nahbar Pro oder Max")
.font(.system(size: 15, weight: .medium)) .font(.system(size: 15, weight: .semibold))
.foregroundStyle(theme.accent) .foregroundStyle(theme.accent)
Text(store.isPro Text(store.isPro
? "Auf Max upgraden KI-Analyse freischalten" ? "KI Insights freischalten"
: "KI-Analyse, Themes & mehr") : "KI Insights, Themes & mehr")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundStyle(theme.contentTertiary) .foregroundStyle(theme.contentTertiary)
} }
Spacer() Spacer()
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .medium)) .font(.system(size: 12, weight: .medium))
.foregroundStyle(theme.contentTertiary) .foregroundStyle(theme.accent.opacity(0.5))
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 12) .padding(.vertical, 16)
.background(theme.surfaceCard) .background(
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) 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) .padding(.horizontal, 20)
} }
} }
} }
.sheet(isPresented: $showPaywall) { PaywallView(targeting: store.isPro ? .max : .pro) } }
// Theme picker // MARK: - 2 · Darstellung & Profil
private var darstellungSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "Atmosphäre", icon: "paintpalette") SectionHeader(title: "Darstellung & Profil", icon: "paintpalette")
.padding(.horizontal, 20) .padding(.horizontal, 20)
VStack(spacing: 0) {
// Theme
NavigationLink(destination: ThemePickerView()) { NavigationLink(destination: ThemePickerView()) {
HStack(spacing: 14) { HStack(spacing: 14) {
// Swatch
ZStack(alignment: .bottomTrailing) { ZStack(alignment: .bottomTrailing) {
RoundedRectangle(cornerRadius: 8) RoundedRectangle(cornerRadius: 8)
.fill(NahbarTheme.theme(for: activeThemeID).backgroundPrimary) .fill(NahbarTheme.theme(for: activeThemeID).backgroundPrimary)
@@ -149,18 +192,234 @@ struct SettingsView: View {
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 12) .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) .background(theme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
.padding(.horizontal, 20) .padding(.horizontal, 20)
} }
} }
// App-Schutz // MARK: - 3 · Funktionen
private var funktionenSection: some View {
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "App-Schutz", icon: "lock") SectionHeader(title: "Funktionen", icon: "slider.horizontal.3")
.padding(.horizontal, 20) .padding(.horizontal, 20)
VStack(spacing: 0) { 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 { HStack {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("Code-Schutz") Text("Code-Schutz")
@@ -201,209 +460,18 @@ struct SettingsView: View {
.padding(.vertical, 12) .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() 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)
.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 // iCloud
VStack(alignment: .leading, spacing: 12) {
SectionHeader(title: "iCloud", icon: "icloud")
.padding(.horizontal, 20)
VStack(spacing: 0) {
// Toggle
HStack { HStack {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("iCloud-Sync") Text("iCloud-Sync")
.font(.system(size: 15)) .font(.system(size: 15))
.foregroundStyle(theme.contentPrimary) .foregroundStyle(theme.contentPrimary)
Text(icloudSyncEnabled Text(icloudSyncEnabled
? "Daten werden geräteübergreifend synchronisiert" ? "Geräteübergreifend synchronisiert"
: "Daten werden nur lokal gespeichert") : "Nur lokal gespeichert")
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundStyle(theme.contentTertiary) .foregroundStyle(theme.contentTertiary)
} }
@@ -420,7 +488,6 @@ struct SettingsView: View {
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 12) .padding(.vertical, 12)
// Live-Sync-Status (nur wenn aktiviert)
if icloudSyncEnabled { if icloudSyncEnabled {
RowDivider() RowDivider()
HStack(spacing: 8) { HStack(spacing: 8) {
@@ -437,7 +504,6 @@ struct SettingsView: View {
.padding(.vertical, 10) .padding(.vertical, 10)
} }
// Neustart-Banner wenn Toggle verändert wurde
if icloudToggleChanged { if icloudToggleChanged {
RowDivider() RowDivider()
HStack(spacing: 10) { HStack(spacing: 10) {
@@ -448,15 +514,20 @@ struct SettingsView: View {
.font(.system(size: 12)) .font(.system(size: 12))
.foregroundStyle(theme.contentSecondary) .foregroundStyle(theme.contentSecondary)
Spacer() Spacer()
Button("Jetzt") { Button("Jetzt") { exit(0) }
exit(0)
}
.font(.system(size: 12, weight: .semibold)) .font(.system(size: 12, weight: .semibold))
.foregroundStyle(theme.accent) .foregroundStyle(theme.accent)
} }
.padding(.horizontal, 16) .padding(.horizontal, 16)
.padding(.vertical, 10) .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) .background(theme.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard)) .clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
@@ -465,195 +536,6 @@ struct SettingsView: View {
.animation(.easeInOut(duration: 0.2), value: icloudToggleChanged) .animation(.easeInOut(duration: 0.2), value: icloudToggleChanged)
.animation(.easeInOut(duration: 0.2), value: cloudSyncMonitor.state == .syncing) .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(.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.")
}
}
// MARK: - App Reset (Entwickler-Tool)
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)
// 2. Profil und Kontakte löschen
UserProfileStore.shared.reset()
ContactStore.shared.reset()
// 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()
// 4. App neu starten damit alle States frisch initialisiert werden
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { exit(0) }
} }
} }
@@ -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 // MARK: - Theme Option Row
struct ThemeOptionRow: View { struct ThemeOptionRow: View {