5bc81d5b3b
iOS SwiftUI app with Supabase auth/realtime, Node.js backend, Docker/Supabase self-hosted infrastructure, and APNs scheduler. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
244 lines
9.5 KiB
Swift
244 lines
9.5 KiB
Swift
import SwiftUI
|
|
|
|
struct SettingsView: View {
|
|
@EnvironmentObject var appState: AppState
|
|
@Environment(\.dismiss) var dismiss
|
|
@State private var showLegal = false
|
|
@State private var showDeleteConfirm = false
|
|
@State private var showDeleteFinal = false
|
|
@State private var deletePassword = ""
|
|
@State private var isDeleting = false
|
|
@State private var deleteError: String?
|
|
@State private var notificationsEnabled = false
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
Color.nightBase.ignoresSafeArea()
|
|
|
|
List {
|
|
// Account
|
|
Section {
|
|
if let user = appState.currentUser {
|
|
HStack(spacing: 12) {
|
|
AvatarView(user: user, size: 44)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(user.displayName)
|
|
.font(.nightLabel(15, weight: .semibold))
|
|
.foregroundColor(.nightPrimary)
|
|
Text("@\(user.username)")
|
|
.font(.nightLabel(13))
|
|
.foregroundColor(.nightSecondary)
|
|
}
|
|
}
|
|
.padding(.vertical, 4)
|
|
}
|
|
}
|
|
.listRowBackground(Color.nightSurface)
|
|
|
|
// Benachrichtigungen
|
|
Section("benachrichtigungen") {
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("nightly ping")
|
|
.font(.nightLabel(15))
|
|
.foregroundColor(.nightPrimary)
|
|
Text("Wenn das Fenster öffnet")
|
|
.font(.nightLabel(12))
|
|
.foregroundColor(.nightSecondary)
|
|
}
|
|
Spacer()
|
|
Toggle("", isOn: $notificationsEnabled)
|
|
.tint(.nightPurple)
|
|
}
|
|
|
|
// APNs-Hinweis
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "info.circle")
|
|
.foregroundColor(.nightSecondary)
|
|
.font(.system(size: 13))
|
|
Text("Push-Benachrichtigungen benötigen einen bezahlten Apple Developer Account ($99/Jahr).")
|
|
.font(.nightLabel(12))
|
|
.foregroundColor(.nightSecondary)
|
|
.lineSpacing(3)
|
|
}
|
|
.padding(.vertical, 2)
|
|
}
|
|
.listRowBackground(Color.nightSurface)
|
|
|
|
// Rechtliches
|
|
Section("rechtliches") {
|
|
Button {
|
|
showLegal = true
|
|
} label: {
|
|
HStack {
|
|
Text("Impressum & Datenschutz")
|
|
.font(.nightLabel(15))
|
|
.foregroundColor(.nightPrimary)
|
|
Spacer()
|
|
Image(systemName: "chevron.right")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.nightSecondary)
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Text("Version")
|
|
.font(.nightLabel(15))
|
|
.foregroundColor(.nightPrimary)
|
|
Spacer()
|
|
Text(appVersion)
|
|
.font(.nightMono(13))
|
|
.foregroundColor(.nightSecondary)
|
|
}
|
|
}
|
|
.listRowBackground(Color.nightSurface)
|
|
|
|
// Account-Aktionen
|
|
Section("account") {
|
|
Button {
|
|
appState.signOut()
|
|
dismiss()
|
|
} label: {
|
|
Text("abmelden")
|
|
.font(.nightLabel(15))
|
|
.foregroundColor(.nightPrimary)
|
|
}
|
|
|
|
Button(role: .destructive) {
|
|
showDeleteConfirm = true
|
|
} label: {
|
|
Text("account löschen")
|
|
.font(.nightLabel(15))
|
|
.foregroundColor(.nightRed)
|
|
}
|
|
}
|
|
.listRowBackground(Color.nightSurface)
|
|
}
|
|
.scrollContentBackground(.hidden)
|
|
.listStyle(.insetGrouped)
|
|
}
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .principal) {
|
|
Text("einstellungen")
|
|
.font(.nightTitle(17))
|
|
.foregroundColor(.nightPrimary)
|
|
}
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Fertig") { dismiss() }
|
|
.foregroundColor(.nightSecondary)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showLegal) { LegalView() }
|
|
}
|
|
.preferredColorScheme(.dark)
|
|
// Schritt 1: Erklärung
|
|
.confirmationDialog(
|
|
"Account wirklich löschen?",
|
|
isPresented: $showDeleteConfirm,
|
|
titleVisibility: .visible
|
|
) {
|
|
Button("Ja, Account löschen", role: .destructive) {
|
|
showDeleteFinal = true
|
|
}
|
|
} message: {
|
|
Text("Alle deine Posts, Reaktionen und Daten werden sofort und dauerhaft gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden.")
|
|
}
|
|
// Schritt 2: Passwort bestätigen
|
|
.sheet(isPresented: $showDeleteFinal) {
|
|
DeleteAccountSheet(
|
|
password: $deletePassword,
|
|
isDeleting: isDeleting,
|
|
error: deleteError,
|
|
onDelete: { Task { await deleteAccount() } }
|
|
)
|
|
}
|
|
.onAppear { checkNotificationStatus() }
|
|
}
|
|
|
|
var appVersion: String {
|
|
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"
|
|
}
|
|
|
|
func checkNotificationStatus() {
|
|
Task {
|
|
let settings = await UNUserNotificationCenter.current().notificationSettings()
|
|
notificationsEnabled = settings.authorizationStatus == .authorized
|
|
}
|
|
}
|
|
|
|
func deleteAccount() async {
|
|
isDeleting = true
|
|
deleteError = nil
|
|
defer { isDeleting = false }
|
|
do {
|
|
try await appState.deleteAccount()
|
|
showDeleteFinal = false
|
|
dismiss()
|
|
} catch {
|
|
deleteError = error.localizedDescription
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DeleteAccountSheet: View {
|
|
@Binding var password: String
|
|
let isDeleting: Bool
|
|
let error: String?
|
|
let onDelete: () -> Void
|
|
@Environment(\.dismiss) var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
Color.nightBase.ignoresSafeArea()
|
|
VStack(spacing: 24) {
|
|
Image(systemName: "trash.circle.fill")
|
|
.font(.system(size: 52))
|
|
.foregroundColor(.nightRed)
|
|
|
|
VStack(spacing: 8) {
|
|
Text("Account löschen")
|
|
.font(.nightTitle(22))
|
|
.foregroundColor(.nightPrimary)
|
|
Text("Diese Aktion löscht alle deine Daten dauerhaft. Kein Weg zurück.")
|
|
.font(.nightBody(15))
|
|
.foregroundColor(.nightSecondary)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
|
|
NightlyField(text: $password, placeholder: "passwort bestätigen", icon: "lock", isSecure: true)
|
|
.padding(.horizontal, 24)
|
|
|
|
if let err = error {
|
|
Text(err).font(.nightLabel(13)).foregroundColor(.nightRed)
|
|
}
|
|
|
|
Button {
|
|
onDelete()
|
|
} label: {
|
|
Group {
|
|
if isDeleting { ProgressView().tint(.white) }
|
|
else { Text("endgültig löschen").font(.nightLabel(16, weight: .semibold)).foregroundColor(.white) }
|
|
}
|
|
.frame(maxWidth: .infinity).frame(height: 50)
|
|
.background(Color.nightRed)
|
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
|
}
|
|
.disabled(password.isEmpty || isDeleting)
|
|
.padding(.horizontal, 24)
|
|
}
|
|
.padding(.top, 32)
|
|
}
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button("Abbrechen") { dismiss() }.foregroundColor(.nightSecondary)
|
|
}
|
|
}
|
|
}
|
|
.preferredColorScheme(.dark)
|
|
.presentationDetents([.medium])
|
|
}
|
|
}
|