mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 11:37:17 +00:00
feat(settings): add ask where to save option and overhaul Locations and Downloads UI
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user