Initial commit: nightly iOS app + Supabase backend
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>
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
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])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user