import SwiftUI import SFSafeSymbols /// Settings section detail views, rebuilt as native `Form` content. Each view is /// placed inside a parent `Form`, so it returns `Section`s / rows directly. struct PreferencesSettings: View { @ObservedObject var model: SettingsModel var body: some View { Section(String(localized: L10n.Field.username)) { TextField(String(localized: L10n.Field.username), text: Binding(get: { model.state.username }, set: { model.setUsername($0) })) } if model.state.supportsCustomReceiveFolders { Section(String(localized: L10n.Preferences.receiveFolderTitle)) { LabeledContent { Button(String(localized: L10n.Button.chooseFolder), action: model.chooseReceiveFolder) } label: { Label { Text(model.state.receiveFolder?.displayName ?? String(localized: L10n.Value.unavailable)) .lineLimit(1) .truncationMode(.middle) } icon: { Image(systemSymbol: .folder) } } if !model.isUsingDefaultReceiveFolder { Button(String(localized: L10n.Button.resetDefault), role: .cancel, action: model.resetReceiveFolder) } } } } } struct AppearanceSettings: View { @ObservedObject var model: SettingsModel var body: some View { Section { Picker(String(localized: L10n.Appearance.title), selection: Binding(get: { model.state.themeMode }, set: { model.setThemeMode($0) })) { ForEach(ThemeMode.allCases, id: \.self) { mode in Text(themeModeLabel(mode)).tag(mode) } } .pickerStyle(.inline) .labelsHidden() } } } struct NotificationSettings: View { @ObservedObject var model: SettingsModel var body: some View { Section { Text(String(localized: L10n.Notifications.description)).foregroundStyle(.secondary) switch model.state.notificationPermission { case .notDetermined: Button(String(localized: L10n.Notifications.localTitle), action: model.requestNotifications) case .granted: // Allowed — the OS Settings app is where you disable or fine-tune. Text(String(localized: L10n.Notifications.enabledMessage)).foregroundStyle(.secondary) Button(String(localized: L10n.Button.openSettings), action: model.openNotificationSettings) case .denied: Text(String(localized: L10n.Notifications.permissionDenied)).foregroundStyle(.secondary) Button(String(localized: L10n.Button.openSettings), action: model.openNotificationSettings) case .unsupported: Text(String(localized: L10n.Notifications.unsupported)).foregroundStyle(.secondary) } } .onAppear { model.refreshNotificationPermission() } } } struct StorageSettings: View { @ObservedObject var model: SettingsModel @State private var showDeleteConfirmation = false var body: some View { Section { if let storage = model.state.storage { LabeledContent(String(localized: L10n.Storage.receivedFiles), value: formatBytes(storage.receivedFiles)) LabeledContent(String(localized: L10n.Storage.transferData), value: formatBytes(storage.transferCache)) LabeledContent(String(localized: L10n.Storage.appData), value: formatBytes(storage.appData)) LabeledContent(String(localized: L10n.Storage.temporary), value: formatBytes(storage.temporary)) LabeledContent(String(localized: L10n.Storage.total)) { Text(formatBytes(storage.total)).fontWeight(.semibold) } } else { HStack { Text(String(localized: L10n.Storage.calculating)).foregroundStyle(.secondary) Spacer() ProgressView() } } } footer: { Text(String(localized: L10n.Storage.footer)) } Section { Button(role: .destructive) { showDeleteConfirmation = true } label: { HStack { Text(model.state.isDeletingTransfers ? String(localized: L10n.Storage.deleting) : String(localized: L10n.Storage.deleteTransfers)) if model.state.isDeletingTransfers { Spacer() ProgressView() } } } .disabled(model.state.isDeletingTransfers) } .onAppear { model.loadStorageUsage() } .confirmationDialog( Text(String(localized: L10n.Storage.deleteTransfers)), isPresented: $showDeleteConfirmation, titleVisibility: .visible ) { Button(String(localized: L10n.Storage.deleteTransfers), role: .destructive) { model.deleteAllTransfers() } Button(String(localized: L10n.Button.cancel), role: .cancel) {} } message: { Text(String(localized: L10n.Storage.deleteTransfersDescription)) } } } struct AboutSettings: View { @ObservedObject var model: SettingsModel private static let privacyPolicyURL = URL(string: "https://github.com/vnidrop/vnidrop")! var body: some View { Section { Text(String(localized: L10n.About.tagline)).font(.headline) Text(String(localized: L10n.About.description)).foregroundStyle(.secondary) } Section(String(localized: L10n.About.isTitle)) { AboutPoint(L10n.About.isDirect, .paperplane) AboutPoint(L10n.About.isNoAccount, .personCropCircleBadgeXmark) AboutPoint(L10n.About.isInControl, .checkmarkShield) AboutPoint(L10n.About.isEncrypted, .lock) AboutPoint(L10n.About.isOpen, .chevronLeftForwardslashChevronRight) } Section(String(localized: L10n.About.isntTitle)) { AboutPoint(L10n.About.isntCloud, .icloudSlash) AboutPoint(L10n.About.isntSync, .arrowTriangle2Circlepath) AboutPoint(L10n.About.isntPublic, .megaphone) } Section(String(localized: L10n.About.privacyTitle)) { AboutPoint(L10n.About.privacyCapability, .qrcode) AboutPoint(L10n.About.privacyDeny, .handRaised) AboutPoint(L10n.About.privacyRelay, .antennaRadiowavesLeftAndRight) AboutPoint(L10n.About.privacyLocal, .internaldrive) } Section(String(localized: L10n.About.title)) { LabeledContent(String(localized: L10n.Version.title), value: model.state.appVersion) if let device = model.state.deviceInfo { LabeledContent(String(localized: L10n.Device.modelTitle), value: device.deviceModel ?? "—") LabeledContent(String(localized: L10n.Os.versionTitle), value: device.operatingSystem) } LabeledContent(String(localized: L10n.About.licenseLabel), value: "Apache 2.0") Link(destination: Self.privacyPolicyURL) { Label(String(localized: L10n.About.privacyPolicyLabel), systemSymbol: .handRaised) } } if DiagnosticsBuildConfig.included { Section { Toggle(isOn: Binding( get: { model.state.diagnosticsEnabled }, set: { model.setDiagnosticsEnabled($0) } )) { Text(String(localized: L10n.Diagnostics.title)) } } } } } /// Bug report presented as a sheet from About. Can be dismissed by swipe only /// when empty; otherwise the Cancel button is required. struct BugReportSheet: View { @ObservedObject var model: SettingsModel @Environment(\.dismiss) private var dismiss private var isEmpty: Bool { model.state.bugWhatHappened.isEmpty && model.state.bugExpected.isEmpty && model.state.bugSteps.isEmpty && model.state.bugContact.isEmpty } var body: some View { NavigationStack { Form { BugReportSettings(model: model, onSubmitted: { dismiss() }) } .formStyle(.grouped) .navigationTitle(Text(String(localized: L10n.About.bugReport))) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: L10n.Button.cancel)) { dismiss() } } } } .interactiveDismissDisabled(!isEmpty) } } /// A bullet-style informational row with an SF Symbol and wrapping localized text. private struct AboutPoint: View { let key: String.LocalizationValue let symbol: SFSymbol init(_ key: String.LocalizationValue, _ symbol: SFSymbol) { self.key = key self.symbol = symbol } var body: some View { Label { Text(String(localized: key)) .font(.subheadline) .fixedSize(horizontal: false, vertical: true) } icon: { Image(systemSymbol: symbol).foregroundStyle(.tint) } } } struct BugReportSettings: View { @ObservedObject var model: SettingsModel var onSubmitted: () -> Void = {} var body: some View { Section(String(localized: L10n.Bug.reportWhatLabel)) { TextField("", text: Binding(get: { model.state.bugWhatHappened }, set: { model.setBugWhatHappened($0) }), prompt: Text(String(localized: L10n.Bug.reportWhatHint)), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } Section(String(localized: L10n.Bug.reportExpectedLabel)) { TextField("", text: Binding(get: { model.state.bugExpected }, set: { model.setBugExpected($0) }), prompt: Text(String(localized: L10n.Bug.reportExpectedHint)), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } Section(String(localized: L10n.Bug.reportStepsLabel)) { TextField("", text: Binding(get: { model.state.bugSteps }, set: { model.setBugSteps($0) }), prompt: Text(String(localized: L10n.Bug.reportStepsHint)), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } Section(String(localized: L10n.Bug.reportContactLabel)) { TextField("", text: Binding(get: { model.state.bugContact }, set: { model.setBugContact($0) }), prompt: Text(String(localized: L10n.Bug.reportContactHint))) .labelsHidden() } Section { Toggle(isOn: Binding(get: { model.state.bugIncludeLogs }, set: { model.setBugIncludeLogs($0) })) { Text(String(localized: L10n.Bug.reportIncludeLogs)) } Button(action: { model.submitBugReport(onSuccess: onSubmitted) }) { Text(model.state.isSubmittingBugReport ? String(localized: L10n.Bug.reportSubmitting) : String(localized: L10n.Bug.reportSubmit)) } .disabled(model.state.isSubmittingBugReport) } } }