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:
+418
-421
@@ -21,22 +21,13 @@ struct SettingsView: View {
|
||||
@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,66 +52,118 @@ struct SettingsView: View {
|
||||
.padding(.horizontal, 20)
|
||||
.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) {
|
||||
SectionHeader(title: "Abonnement", icon: "star.fill")
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
if store.isMax {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
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))
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
.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(.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: 2) {
|
||||
Text("Pro oder Max-Abo")
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
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
|
||||
? "Auf Max upgraden – KI-Analyse freischalten"
|
||||
: "KI-Analyse, Themes & mehr")
|
||||
? "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.contentTertiary)
|
||||
.foregroundStyle(theme.accent.opacity(0.5))
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(theme.surfaceCard)
|
||||
.clipShape(RoundedRectangle(cornerRadius: theme.radiusCard))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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) {
|
||||
SectionHeader(title: "Atmosphäre", icon: "paintpalette")
|
||||
SectionHeader(title: "Darstellung & Profil", icon: "paintpalette")
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Theme
|
||||
NavigationLink(destination: ThemePickerView()) {
|
||||
HStack(spacing: 14) {
|
||||
// Swatch
|
||||
ZStack(alignment: .bottomTrailing) {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(NahbarTheme.theme(for: activeThemeID).backgroundPrimary)
|
||||
@@ -149,18 +192,234 @@ struct SettingsView: View {
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
|
||||
// App-Schutz
|
||||
// MARK: - 3 · Funktionen
|
||||
|
||||
private var funktionenSection: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
SectionHeader(title: "App-Schutz", icon: "lock")
|
||||
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")
|
||||
@@ -201,209 +460,18 @@ struct SettingsView: View {
|
||||
.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)
|
||||
.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")
|
||||
? "Geräteübergreifend synchronisiert"
|
||||
: "Nur lokal gespeichert")
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(theme.contentTertiary)
|
||||
}
|
||||
@@ -420,7 +488,6 @@ struct SettingsView: View {
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
|
||||
// Live-Sync-Status (nur wenn aktiviert)
|
||||
if icloudSyncEnabled {
|
||||
RowDivider()
|
||||
HStack(spacing: 8) {
|
||||
@@ -437,7 +504,6 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
|
||||
// Neustart-Banner wenn Toggle verändert wurde
|
||||
if icloudToggleChanged {
|
||||
RowDivider()
|
||||
HStack(spacing: 10) {
|
||||
@@ -448,15 +514,20 @@ struct SettingsView: View {
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(theme.contentSecondary)
|
||||
Spacer()
|
||||
Button("Jetzt") {
|
||||
exit(0)
|
||||
}
|
||||
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))
|
||||
@@ -465,195 +536,6 @@ struct SettingsView: View {
|
||||
.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(.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
|
||||
|
||||
struct ThemeOptionRow: View {
|
||||
@@ -821,7 +818,7 @@ enum AppLanguage: String, CaseIterable {
|
||||
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 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.
|
||||
|
||||
Reference in New Issue
Block a user