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)
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"
@@ -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)
@@ -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")
}
}