From 85f55db66fc1b7a6e91d2d7251a86a9472ef372c Mon Sep 17 00:00:00 2001 From: NimBold Date: Wed, 10 Jun 2026 05:43:13 +0330 Subject: [PATCH] feat(settings): add ask where to save option and overhaul Locations and Downloads UI --- Sources/Firelink/AppSettings.swift | 4 +- .../Settings/DownloadSettingsPane.swift | 93 ++++++++++--------- .../Settings/LocationsSettingsPane.swift | 52 +++++------ 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/Sources/Firelink/AppSettings.swift b/Sources/Firelink/AppSettings.swift index e345ce9..dfc48ff 100644 --- a/Sources/Firelink/AppSettings.swift +++ b/Sources/Firelink/AppSettings.swift @@ -258,7 +258,7 @@ final class AppSettings: ObservableObject { downloadDirectories = Self.decodeDirectories(stored.downloadDirectories) granted = stored.isKeychainAccessGranted ?? false isKeychainAccessGranted = granted - askWhereToSaveEachFile = stored.askWhereToSaveEachFile ?? true + askWhereToSaveEachFile = stored.askWhereToSaveEachFile ?? false } else { appTheme = .system appFontSize = .standard @@ -278,7 +278,7 @@ final class AppSettings: ObservableObject { downloadDirectories = Self.defaultDirectories() granted = false isKeychainAccessGranted = granted - askWhereToSaveEachFile = true + askWhereToSaveEachFile = false } let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" diff --git a/Sources/Firelink/Settings/DownloadSettingsPane.swift b/Sources/Firelink/Settings/DownloadSettingsPane.swift index 89d055e..3828676 100644 --- a/Sources/Firelink/Settings/DownloadSettingsPane.swift +++ b/Sources/Firelink/Settings/DownloadSettingsPane.swift @@ -5,28 +5,28 @@ struct DownloadSettingsPane: View { var body: some View { Form { - Section("Connections") { - Stepper( - "Default connections per server: \(settings.perServerConnections)", - value: $settings.perServerConnections, - in: 1...16 - ) - Text("Used as the default for new downloads. The Add Downloads window can override it per batch.") - .font(.caption) - .foregroundStyle(.secondary) - - Stepper( - "Parallel downloads: \(settings.maxConcurrentDownloads)", - value: $settings.maxConcurrentDownloads, - in: 1...12 - ) - Text("Controls how many files Firelink downloads at the same time.") - .font(.caption) - .foregroundStyle(.secondary) - } - - Section("Bandwidth") { - LabeledContent("Global speed limit") { + Section { + LabeledContent { + Stepper("\(settings.perServerConnections)", value: $settings.perServerConnections, in: 1...16) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("Default connections:") + Text("For new downloads") + .font(.caption) + .foregroundStyle(.secondary) + } + } + LabeledContent { + Stepper("\(settings.maxConcurrentDownloads)", value: $settings.maxConcurrentDownloads, in: 1...12) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("Parallel downloads:") + Text("Max simultaneous active files") + .font(.caption) + .foregroundStyle(.secondary) + } + } + LabeledContent { HStack { TextField("0", value: $settings.globalSpeedLimitKiBPerSecond, format: .number) .textFieldStyle(.roundedBorder) @@ -35,27 +35,36 @@ struct DownloadSettingsPane: View { Text("KiB/s") .foregroundStyle(.secondary) } + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("Global speed limit:") + Text("0 = unlimited speed") + .font(.caption) + .foregroundStyle(.secondary) + } } - Text("Set to 0 for unlimited speed. This limit is divided across currently active downloads.") - .font(.caption) - .foregroundStyle(.secondary) - } - - Section("Recovery") { - Stepper( - "Automatic retries: \(settings.maxAutomaticRetries)", - value: $settings.maxAutomaticRetries, - in: 0...10 - ) - Text("Number of times to retry a download automatically if the connection fails.") - .font(.caption) - .foregroundStyle(.secondary) - } - - Section("Notifications") { - Toggle("Show notification when download completes", isOn: $settings.showNotifications) - Toggle("Play sound when download completes", isOn: $settings.playCompletionSound) - .disabled(!settings.showNotifications) + LabeledContent { + Stepper("\(settings.maxAutomaticRetries)", value: $settings.maxAutomaticRetries, in: 0...10) + } label: { + VStack(alignment: .leading, spacing: 2) { + Text("Automatic retries:") + Text("If a connection fails") + .font(.caption) + .foregroundStyle(.secondary) + } + } + Toggle(isOn: $settings.showNotifications) { + VStack(alignment: .leading, spacing: 2) { + Text("Show notification when download completes") + Text("Alerts you in Notification Center") + .font(.caption) + .foregroundStyle(.secondary) + } + } + Toggle(isOn: $settings.playCompletionSound) { + Text("Play sound when download completes") + } + .disabled(!settings.showNotifications) } } .formStyle(.grouped) diff --git a/Sources/Firelink/Settings/LocationsSettingsPane.swift b/Sources/Firelink/Settings/LocationsSettingsPane.swift index f7f08f7..5ae0317 100644 --- a/Sources/Firelink/Settings/LocationsSettingsPane.swift +++ b/Sources/Firelink/Settings/LocationsSettingsPane.swift @@ -6,15 +6,13 @@ struct LocationsSettingsPane: View { var body: some View { Form { - Section(footer: Text("When enabled, you can choose the download location each time you add a download.")) { + Section(footer: Text("When enabled, you can choose the download location each time you add a download. Otherwise, files are saved automatically.")) { Toggle("Ask where to save each file before downloading", isOn: $settings.askWhereToSaveEachFile) } - - Section(header: Text("Default Locations"), footer: Text("Automatically sets the folder for all categories within the selected base folder.")) { + + Section(footer: Text("Folders will be created automatically when saving.")) { BulkDirectoryPickerRow() - } - - Section(header: Text("Category Locations"), footer: Text("Folders will be created automatically when saving.")) { + ForEach(DownloadCategory.allCases, id: \.self) { category in DirectoryPickerRow(category: category) } @@ -39,12 +37,13 @@ struct DirectoryPickerRow: View { @State private var message = "" var body: some View { - VStack(alignment: .leading, spacing: 4) { - LabeledContent { + LabeledContent { + VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { - TextField("", text: $path) + TextField("Folder path", text: $path, prompt: Text("Folder path")) .labelsHidden() .textFieldStyle(.roundedBorder) + .multilineTextAlignment(.leading) .font(.system(.body, design: .monospaced)) .onSubmit { applyPath() @@ -54,15 +53,15 @@ struct DirectoryPickerRow: View { selectFolder() } } - } label: { - Label(category.rawValue, systemImage: category.symbolName) - } - if let displayMessage = message.isEmpty ? statusMessage(for: path) : message, !displayMessage.isEmpty { - Text(displayMessage) - .font(.caption) - .foregroundStyle(isErrorMessage(displayMessage) ? .red : .secondary) + if let displayMessage = message.isEmpty ? statusMessage(for: path) : message, !displayMessage.isEmpty { + Text(displayMessage) + .font(.caption) + .foregroundStyle(isErrorMessage(displayMessage) ? .red : .secondary) + } } + } label: { + Label(category.rawValue, systemImage: category.symbolName) } .onAppear { syncPathFromSettings() @@ -162,12 +161,13 @@ struct BulkDirectoryPickerRow: View { @State private var message = "" var body: some View { - VStack(alignment: .leading, spacing: 4) { - LabeledContent { + LabeledContent { + VStack(alignment: .leading, spacing: 4) { HStack(spacing: 8) { - TextField("", text: $path) + TextField("Base folder path", text: $path, prompt: Text("Base folder path")) .labelsHidden() .textFieldStyle(.roundedBorder) + .multilineTextAlignment(.leading) .font(.system(.body, design: .monospaced)) .onSubmit { applyPath() @@ -177,15 +177,15 @@ struct BulkDirectoryPickerRow: View { selectFolder() } } - } label: { - Label("All Categories", systemImage: "folder.fill.badge.plus") - } - if !message.isEmpty { - Text(message) - .font(.caption) - .foregroundStyle(isErrorMessage(message) ? .red : .secondary) + if !message.isEmpty { + Text(message) + .font(.caption) + .foregroundStyle(isErrorMessage(message) ? .red : .secondary) + } } + } label: { + Label("All Categories", systemImage: "folder.fill.badge.plus") } }