feat(settings): add ask where to save option and overhaul Locations and Downloads UI

This commit is contained in:
NimBold
2026-06-10 05:43:13 +03:30
parent cc89f48221
commit 85f55db66f
3 changed files with 79 additions and 70 deletions
+2 -2
View File
@@ -258,7 +258,7 @@ final class AppSettings: ObservableObject {
downloadDirectories = Self.decodeDirectories(stored.downloadDirectories) downloadDirectories = Self.decodeDirectories(stored.downloadDirectories)
granted = stored.isKeychainAccessGranted ?? false granted = stored.isKeychainAccessGranted ?? false
isKeychainAccessGranted = granted isKeychainAccessGranted = granted
askWhereToSaveEachFile = stored.askWhereToSaveEachFile ?? true askWhereToSaveEachFile = stored.askWhereToSaveEachFile ?? false
} else { } else {
appTheme = .system appTheme = .system
appFontSize = .standard appFontSize = .standard
@@ -278,7 +278,7 @@ final class AppSettings: ObservableObject {
downloadDirectories = Self.defaultDirectories() downloadDirectories = Self.defaultDirectories()
granted = false granted = false
isKeychainAccessGranted = granted isKeychainAccessGranted = granted
askWhereToSaveEachFile = true askWhereToSaveEachFile = false
} }
let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown" let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
@@ -5,28 +5,28 @@ struct DownloadSettingsPane: View {
var body: some View { var body: some View {
Form { Form {
Section("Connections") { Section {
Stepper( LabeledContent {
"Default connections per server: \(settings.perServerConnections)", Stepper("\(settings.perServerConnections)", value: $settings.perServerConnections, in: 1...16)
value: $settings.perServerConnections, } label: {
in: 1...16 VStack(alignment: .leading, spacing: 2) {
) Text("Default connections:")
Text("Used as the default for new downloads. The Add Downloads window can override it per batch.") Text("For new downloads")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
}
Stepper( }
"Parallel downloads: \(settings.maxConcurrentDownloads)", LabeledContent {
value: $settings.maxConcurrentDownloads, Stepper("\(settings.maxConcurrentDownloads)", value: $settings.maxConcurrentDownloads, in: 1...12)
in: 1...12 } label: {
) VStack(alignment: .leading, spacing: 2) {
Text("Controls how many files Firelink downloads at the same time.") Text("Parallel downloads:")
.font(.caption) Text("Max simultaneous active files")
.foregroundStyle(.secondary) .font(.caption)
} .foregroundStyle(.secondary)
}
Section("Bandwidth") { }
LabeledContent("Global speed limit") { LabeledContent {
HStack { HStack {
TextField("0", value: $settings.globalSpeedLimitKiBPerSecond, format: .number) TextField("0", value: $settings.globalSpeedLimitKiBPerSecond, format: .number)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@@ -35,27 +35,36 @@ struct DownloadSettingsPane: View {
Text("KiB/s") Text("KiB/s")
.foregroundStyle(.secondary) .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.") LabeledContent {
.font(.caption) Stepper("\(settings.maxAutomaticRetries)", value: $settings.maxAutomaticRetries, in: 0...10)
.foregroundStyle(.secondary) } label: {
} VStack(alignment: .leading, spacing: 2) {
Text("Automatic retries:")
Section("Recovery") { Text("If a connection fails")
Stepper( .font(.caption)
"Automatic retries: \(settings.maxAutomaticRetries)", .foregroundStyle(.secondary)
value: $settings.maxAutomaticRetries, }
in: 0...10 }
) Toggle(isOn: $settings.showNotifications) {
Text("Number of times to retry a download automatically if the connection fails.") VStack(alignment: .leading, spacing: 2) {
.font(.caption) Text("Show notification when download completes")
.foregroundStyle(.secondary) Text("Alerts you in Notification Center")
} .font(.caption)
.foregroundStyle(.secondary)
Section("Notifications") { }
Toggle("Show notification when download completes", isOn: $settings.showNotifications) }
Toggle("Play sound when download completes", isOn: $settings.playCompletionSound) Toggle(isOn: $settings.playCompletionSound) {
.disabled(!settings.showNotifications) Text("Play sound when download completes")
}
.disabled(!settings.showNotifications)
} }
} }
.formStyle(.grouped) .formStyle(.grouped)
@@ -6,15 +6,13 @@ struct LocationsSettingsPane: View {
var body: some View { var body: some View {
Form { 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) 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() BulkDirectoryPickerRow()
}
Section(header: Text("Category Locations"), footer: Text("Folders will be created automatically when saving.")) {
ForEach(DownloadCategory.allCases, id: \.self) { category in ForEach(DownloadCategory.allCases, id: \.self) { category in
DirectoryPickerRow(category: category) DirectoryPickerRow(category: category)
} }
@@ -39,12 +37,13 @@ struct DirectoryPickerRow: View {
@State private var message = "" @State private var message = ""
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { LabeledContent {
LabeledContent { VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) { HStack(spacing: 8) {
TextField("", text: $path) TextField("Folder path", text: $path, prompt: Text("Folder path"))
.labelsHidden() .labelsHidden()
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
.onSubmit { .onSubmit {
applyPath() applyPath()
@@ -54,15 +53,15 @@ struct DirectoryPickerRow: View {
selectFolder() selectFolder()
} }
} }
} label: {
Label(category.rawValue, systemImage: category.symbolName)
}
if let displayMessage = message.isEmpty ? statusMessage(for: path) : message, !displayMessage.isEmpty { if let displayMessage = message.isEmpty ? statusMessage(for: path) : message, !displayMessage.isEmpty {
Text(displayMessage) Text(displayMessage)
.font(.caption) .font(.caption)
.foregroundStyle(isErrorMessage(displayMessage) ? .red : .secondary) .foregroundStyle(isErrorMessage(displayMessage) ? .red : .secondary)
}
} }
} label: {
Label(category.rawValue, systemImage: category.symbolName)
} }
.onAppear { .onAppear {
syncPathFromSettings() syncPathFromSettings()
@@ -162,12 +161,13 @@ struct BulkDirectoryPickerRow: View {
@State private var message = "" @State private var message = ""
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 4) { LabeledContent {
LabeledContent { VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 8) { HStack(spacing: 8) {
TextField("", text: $path) TextField("Base folder path", text: $path, prompt: Text("Base folder path"))
.labelsHidden() .labelsHidden()
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
.onSubmit { .onSubmit {
applyPath() applyPath()
@@ -177,15 +177,15 @@ struct BulkDirectoryPickerRow: View {
selectFolder() selectFolder()
} }
} }
} label: {
Label("All Categories", systemImage: "folder.fill.badge.plus")
}
if !message.isEmpty { if !message.isEmpty {
Text(message) Text(message)
.font(.caption) .font(.caption)
.foregroundStyle(isErrorMessage(message) ? .red : .secondary) .foregroundStyle(isErrorMessage(message) ? .red : .secondary)
}
} }
} label: {
Label("All Categories", systemImage: "folder.fill.badge.plus")
} }
} }