mirror of
https://github.com/sudosylabs/vnidrop.git
synced 2026-08-05 10:29:58 +02:00
feat(apple): storage screen, About content, and fixes
- Settings: add Storage screen (size breakdown + delete-all-transfers) and expand About (what it is/isn't, privacy & security, license/source) - Move Report a bug to a toolbar sheet (cancel-only unless empty) - Send progress: derive the list-row bar from receiver delivery status so it clears once every receiver completes - Fixes: iPad orientations, onChange(of:) iOS 17 API, weak-self captures, invalid SF Symbol, macOS bug-report form labels; bump core build target to match the app (18.2/15.0)
This commit is contained in:
@@ -57,17 +57,106 @@ struct NotificationSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
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 sourceURL = 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.sourceURL) {
|
||||
Label(String(localized: "about_source_label"), systemImage: "link")
|
||||
}
|
||||
}
|
||||
|
||||
if DiagnosticsBuildConfig.included {
|
||||
Section {
|
||||
Toggle(isOn: Binding(
|
||||
@@ -78,42 +167,94 @@ struct AboutSettings: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
NavigationLink(value: SettingsSection.bugReport) {
|
||||
Text(LocalizedStringKey("about_bug_report"))
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(String(localized: "bug_report_what_label"),
|
||||
text: Binding(get: { model.state.bugWhatHappened }, set: model.setBugWhatHappened), axis: .vertical)
|
||||
TextField("", text: Binding(get: { model.state.bugWhatHappened }, set: model.setBugWhatHappened),
|
||||
prompt: Text(LocalizedStringKey("bug_report_what_hint")), axis: .vertical)
|
||||
.lineLimit(3, reservesSpace: true)
|
||||
.labelsHidden()
|
||||
}
|
||||
Section(String(localized: "bug_report_expected_label")) {
|
||||
TextField(String(localized: "bug_report_expected_label"),
|
||||
text: Binding(get: { model.state.bugExpected }, set: model.setBugExpected), axis: .vertical)
|
||||
TextField("", text: Binding(get: { model.state.bugExpected }, set: model.setBugExpected),
|
||||
prompt: Text(LocalizedStringKey("bug_report_expected_hint")), axis: .vertical)
|
||||
.lineLimit(3, reservesSpace: true)
|
||||
.labelsHidden()
|
||||
}
|
||||
Section(String(localized: "bug_report_steps_label")) {
|
||||
TextField(String(localized: "bug_report_steps_label"),
|
||||
text: Binding(get: { model.state.bugSteps }, set: model.setBugSteps), axis: .vertical)
|
||||
TextField("", text: Binding(get: { model.state.bugSteps }, set: model.setBugSteps),
|
||||
prompt: Text(LocalizedStringKey("bug_report_steps_hint")), axis: .vertical)
|
||||
.lineLimit(3, reservesSpace: true)
|
||||
.labelsHidden()
|
||||
}
|
||||
Section(String(localized: "bug_report_contact_label")) {
|
||||
TextField(String(localized: "bug_report_contact_label"),
|
||||
text: Binding(get: { model.state.bugContact }, set: model.setBugContact))
|
||||
TextField("", text: Binding(get: { model.state.bugContact }, set: model.setBugContact),
|
||||
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) {
|
||||
Button(action: { model.submitBugReport(onSuccess: onSubmitted) }) {
|
||||
Text(model.state.isSubmittingBugReport
|
||||
? String(localized: "bug_report_submitting") : String(localized: "bug_report_submit"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user