Files
vnidrop/apple/VniDrop/Features/Settings/SettingsScreen.swift
Hammed Abass cbace73908 feat(network): support custom relay servers
Add strict custom Iroh relay profiles with safe restart and rollback across the Rust core, Compose apps, and Apple apps. Preserve multi-relay invitations and fail closed on configuration or recovery mismatches.
2026-07-23 14:27:40 +02:00

158 lines
4.2 KiB
Swift

import SwiftUI
/// Settings screen, rebuilt on a native `Form` with `NavigationStack` push
/// navigation. The model stays the source of truth via a derived path binding.
struct SettingsScreen: View {
@ObservedObject var model: SettingsModel
let windowClass: WindowClass
@State private var showBugReport = false
private var path: Binding<[SettingsSection]> {
Binding(
get: {
switch model.state.selectedSection {
case .overview: return []
case .bugReport: return [.about, .bugReport]
case let section: return [section]
}
},
set: { newPath in model.selectSection(newPath.last ?? .overview) }
)
}
var body: some View {
NavigationStack(path: path) {
Form {
Section {
NavigationLink(value: SettingsSection.preferences) {
SettingsRow(icon: "person.crop.circle", title: String(localized: "preferences_title"), value: model.state.username)
}
NavigationLink(value: SettingsSection.appearance) {
SettingsRow(icon: "sun.max", title: String(localized: "appearance_title"), value: themeModeLabel(model.state.themeMode))
}
}
Section {
NavigationLink(value: SettingsSection.notifications) {
SettingsRow(icon: "bell", title: String(localized: "notifications_title"), value: nil)
}
NavigationLink(value: SettingsSection.storage) {
SettingsRow(icon: "internaldrive", title: String(localized: "storage_title"), value: nil)
}
}
Section(String(localized: "settings_advanced_title")) {
NavigationLink(value: SettingsSection.network) {
SettingsRow(
icon: "network",
title: String(localized: "settings_network_title"),
value: relayModeLabel(model.state.relayMode)
)
}
}
Section {
NavigationLink(value: SettingsSection.about) {
SettingsRow(icon: "info.circle", title: String(localized: "about_title"), value: nil)
}
}
}
.formStyle(.grouped)
.navigationTitle(Text(LocalizedStringKey("settings_title")))
.navigationDestination(for: SettingsSection.self) { section in
sectionForm(section)
}
}
}
@ViewBuilder
private func sectionForm(_ section: SettingsSection) -> some View {
let content = Form {
SettingsSectionContent(model: model, section: section)
}
.formStyle(.grouped)
.navigationTitle(Text(LocalizedStringKey(section.titleKey)))
if section == .about {
content
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
showBugReport = true
} label: {
Label(String(localized: "about_bug_report"), systemImage: "ladybug")
}
}
}
.sheet(isPresented: $showBugReport) {
BugReportSheet(model: model)
}
} else {
content
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
}
private struct SettingsSectionContent: View {
@ObservedObject var model: SettingsModel
let section: SettingsSection
var body: some View {
switch section {
case .overview:
EmptyView()
case .preferences:
PreferencesSettings(model: model)
case .appearance:
AppearanceSettings(model: model)
case .notifications:
NotificationSettings(model: model)
case .network:
NetworkSettings(model: model)
case .storage:
StorageSettings(model: model)
case .about:
AboutSettings(model: model)
case .bugReport:
BugReportSettings(model: model)
}
}
}
func relayModeLabel(_ mode: RelayPreferenceMode) -> String {
switch mode {
case .automatic: return String(localized: "relay_mode_automatic")
case .custom: return String(localized: "relay_mode_custom")
}
}
struct SettingsRow: View {
let icon: String
let title: String
let value: String?
var body: some View {
HStack(spacing: 12) {
Image(systemName: icon)
.foregroundStyle(.tint)
.frame(width: 26)
Text(title).foregroundStyle(.primary)
Spacer()
if let value {
Text(value).foregroundStyle(.secondary).lineLimit(1)
}
}
}
}
func themeModeLabel(_ mode: ThemeMode) -> String {
switch mode {
case .system: return String(localized: "appearance_system_mode")
case .light: return String(localized: "appearance_light_mode")
case .dark: return String(localized: "appearance_dark_mode")
}
}