import SwiftUI /// 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: "field_username")) { TextField(String(localized: "field_username"), text: Binding(get: { model.state.username }, set: { model.setUsername($0) })) } if model.state.supportsCustomReceiveFolders { Section(String(localized: "preferences_receive_folder_title")) { Text(model.state.receiveFolder?.displayName ?? String(localized: "value_unavailable")) .foregroundStyle(.secondary) Button(String(localized: "button_choose_folder"), action: model.chooseReceiveFolder) Button(String(localized: "button_reset_default"), action: model.resetReceiveFolder) } } } } struct AppearanceSettings: View { @ObservedObject var model: SettingsModel var body: some View { Section { Picker(String(localized: "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 { Toggle(isOn: Binding( get: { model.state.notificationsEnabled }, set: { model.setNotificationsEnabled($0) } )) { Text(LocalizedStringKey("notifications_local_title")) } if model.state.notificationPermission == .denied { Button(String(localized: "button_open_settings"), action: model.openNotificationSettings) } } } } 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: "storage_received_files"), value: formatBytes(storage.receivedFiles)) LabeledContent(String(localized: "storage_transfer_data"), value: formatBytes(storage.transferData)) LabeledContent(String(localized: "storage_temporary"), value: formatBytes(storage.temporary)) LabeledContent(String(localized: "storage_total")) { Text(formatBytes(storage.total)).fontWeight(.semibold) } } else { HStack { Text(LocalizedStringKey("storage_calculating")).foregroundStyle(.secondary) Spacer() ProgressView() } } } footer: { Text(LocalizedStringKey("storage_footer")) } Section { Button(role: .destructive) { showDeleteConfirmation = true } label: { HStack { Text(model.state.isDeletingTransfers ? String(localized: "storage_deleting") : String(localized: "storage_delete_transfers")) if model.state.isDeletingTransfers { Spacer() ProgressView() } } } .disabled(model.state.isDeletingTransfers) } .onAppear { model.loadStorageUsage() } .confirmationDialog( Text(LocalizedStringKey("storage_delete_transfers")), isPresented: $showDeleteConfirmation, titleVisibility: .visible ) { Button(String(localized: "storage_delete_transfers"), role: .destructive) { model.deleteAllTransfers() } Button(String(localized: "button_cancel"), role: .cancel) {} } message: { Text(LocalizedStringKey("storage_delete_transfers_description")) } } } struct AboutSettings: View { @ObservedObject var model: SettingsModel private static let privacyPolicyURL = URL(string: "https://github.com/vnidrop/vnidrop")! var body: some View { Section { Text(LocalizedStringKey("about_tagline")).font(.headline) Text(LocalizedStringKey("about_description")).foregroundStyle(.secondary) } Section(String(localized: "about_is_title")) { AboutPoint("about_is_direct", "paperplane") AboutPoint("about_is_no_account", "person.crop.circle.badge.xmark") AboutPoint("about_is_in_control", "checkmark.shield") AboutPoint("about_is_encrypted", "lock") AboutPoint("about_is_open", "chevron.left.forwardslash.chevron.right") } Section(String(localized: "about_isnt_title")) { AboutPoint("about_isnt_cloud", "icloud.slash") AboutPoint("about_isnt_sync", "arrow.triangle.2.circlepath") AboutPoint("about_isnt_public", "megaphone") } Section(String(localized: "about_privacy_title")) { AboutPoint("about_privacy_capability", "qrcode") AboutPoint("about_privacy_deny", "hand.raised") AboutPoint("about_privacy_relay", "antenna.radiowaves.left.and.right") AboutPoint("about_privacy_local", "internaldrive") } Section(String(localized: "about_title")) { LabeledContent(String(localized: "version_title"), value: model.state.appVersion) if let device = model.state.deviceInfo { LabeledContent(String(localized: "device_model_title"), value: device.deviceModel ?? "—") LabeledContent(String(localized: "os_version_title"), value: device.operatingSystem) } LabeledContent(String(localized: "about_license_label"), value: "Apache 2.0") Link(destination: Self.privacyPolicyURL) { Label(String(localized: "about_privacy_policy_label"), systemImage: "hand.raised") } } if DiagnosticsBuildConfig.included { Section { Toggle(isOn: Binding( get: { model.state.diagnosticsEnabled }, set: { model.setDiagnosticsEnabled($0) } )) { Text(LocalizedStringKey("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(LocalizedStringKey("about_bug_report"))) #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif .toolbar { ToolbarItem(placement: .cancellationAction) { Button(String(localized: "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 let symbol: String init(_ key: String, _ symbol: String) { self.key = key self.symbol = symbol } var body: some View { Label { Text(LocalizedStringKey(key)) .font(.subheadline) .fixedSize(horizontal: false, vertical: true) } icon: { Image(systemName: symbol).foregroundStyle(.tint) } } } struct BugReportSettings: View { @ObservedObject var model: SettingsModel var onSubmitted: () -> Void = {} var body: some View { Section(String(localized: "bug_report_what_label")) { TextField("", text: Binding(get: { model.state.bugWhatHappened }, set: { model.setBugWhatHappened($0) }), prompt: Text(LocalizedStringKey("bug_report_what_hint")), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } Section(String(localized: "bug_report_expected_label")) { TextField("", text: Binding(get: { model.state.bugExpected }, set: { model.setBugExpected($0) }), prompt: Text(LocalizedStringKey("bug_report_expected_hint")), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } Section(String(localized: "bug_report_steps_label")) { TextField("", text: Binding(get: { model.state.bugSteps }, set: { model.setBugSteps($0) }), prompt: Text(LocalizedStringKey("bug_report_steps_hint")), axis: .vertical) .lineLimit(3, reservesSpace: true) .labelsHidden() } Section(String(localized: "bug_report_contact_label")) { TextField("", text: Binding(get: { model.state.bugContact }, set: { model.setBugContact($0) }), prompt: Text(LocalizedStringKey("bug_report_contact_hint"))) .labelsHidden() } Section { Toggle(isOn: Binding(get: { model.state.bugIncludeLogs }, set: { model.setBugIncludeLogs($0) })) { Text(LocalizedStringKey("bug_report_include_logs")) } Button(action: { model.submitBugReport(onSuccess: onSubmitted) }) { Text(model.state.isSubmittingBugReport ? String(localized: "bug_report_submitting") : String(localized: "bug_report_submit")) } .disabled(model.state.isSubmittingBugReport) } } }