mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-28 13:00:10 +00:00
372 lines
12 KiB
Swift
372 lines
12 KiB
Swift
import SwiftUI
|
|
|
|
private enum SettingsSection: String, CaseIterable, Hashable {
|
|
case downloads = "Downloads"
|
|
case network = "Network"
|
|
case locations = "Locations"
|
|
case siteLogins = "Site Logins"
|
|
case power = "Power"
|
|
case engine = "Engine"
|
|
|
|
var symbolName: String {
|
|
switch self {
|
|
case .downloads: "arrow.down.circle"
|
|
case .network: "network"
|
|
case .locations: "folder"
|
|
case .siteLogins: "key.fill"
|
|
case .power: "moon.zzz"
|
|
case .engine: "terminal"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SettingsView: View {
|
|
@State private var selection: SettingsSection = .downloads
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
Text("Settings")
|
|
.font(.largeTitle.weight(.semibold))
|
|
.padding(.horizontal, 28)
|
|
.padding(.top, 26)
|
|
.padding(.bottom, 18)
|
|
|
|
Divider()
|
|
|
|
HStack(spacing: 0) {
|
|
settingsSidebar
|
|
|
|
Divider()
|
|
|
|
ScrollView {
|
|
selectedPane
|
|
.frame(maxWidth: 720, alignment: .leading)
|
|
.padding(28)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
private var settingsSidebar: some View {
|
|
List(selection: $selection) {
|
|
Section("Preferences") {
|
|
ForEach(SettingsSection.allCases, id: \.self) { section in
|
|
Label(section.rawValue, systemImage: section.symbolName)
|
|
.tag(section)
|
|
}
|
|
}
|
|
}
|
|
.listStyle(.sidebar)
|
|
.frame(width: 210)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var selectedPane: some View {
|
|
switch selection {
|
|
case .downloads:
|
|
DownloadSettingsPane()
|
|
case .network:
|
|
NetworkSettingsPane()
|
|
case .locations:
|
|
LocationsSettingsPane()
|
|
case .siteLogins:
|
|
SiteLoginsSettingsPane()
|
|
case .power:
|
|
PowerSettingsPane()
|
|
case .engine:
|
|
EngineSettingsPane()
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct EngineSettingsPane: View {
|
|
private var executableURL: URL? {
|
|
Aria2DownloadEngine.findExecutable()
|
|
}
|
|
|
|
private var version: String {
|
|
Aria2DownloadEngine.versionString() ?? "Unavailable"
|
|
}
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
LabeledContent("Status") {
|
|
Label(
|
|
executableURL == nil ? "Missing" : "Ready",
|
|
systemImage: executableURL == nil ? "exclamationmark.triangle.fill" : "checkmark.seal.fill"
|
|
)
|
|
.foregroundStyle(executableURL == nil ? .orange : .green)
|
|
}
|
|
|
|
LabeledContent("Binary") {
|
|
Text(executableURL?.path ?? "Not found")
|
|
.font(.system(.body, design: .monospaced))
|
|
.textSelection(.enabled)
|
|
}
|
|
|
|
LabeledContent("Version") {
|
|
Text(version)
|
|
.font(.system(.body, design: .monospaced))
|
|
.textSelection(.enabled)
|
|
}
|
|
|
|
if executableURL == nil {
|
|
Text("Install aria2 with Homebrew or bundle aria2c inside the app resources.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
}
|
|
|
|
private struct NetworkSettingsPane: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
Picker("Proxy", selection: proxyBinding(\.mode)) {
|
|
ForEach(ProxyMode.allCases, id: \.self) { mode in
|
|
Text(mode.title)
|
|
.tag(mode)
|
|
}
|
|
}
|
|
.pickerStyle(.radioGroup)
|
|
|
|
if settings.proxySettings.mode == .custom {
|
|
Picker("Proxy type", selection: proxyBinding(\.type)) {
|
|
ForEach(ProxyType.allCases, id: \.self) { type in
|
|
Text(type.title)
|
|
.tag(type)
|
|
}
|
|
}
|
|
|
|
Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10) {
|
|
GridRow {
|
|
Text("IP or Host")
|
|
TextField("127.0.0.1", text: proxyBinding(\.host))
|
|
.textFieldStyle(.roundedBorder)
|
|
.font(.system(.body, design: .monospaced))
|
|
}
|
|
|
|
GridRow {
|
|
Text("Port")
|
|
TextField("8080", value: proxyBinding(\.port), format: .number)
|
|
.textFieldStyle(.roundedBorder)
|
|
.frame(width: 110)
|
|
}
|
|
}
|
|
}
|
|
|
|
Text(networkSummary)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
|
|
private var networkSummary: String {
|
|
switch settings.proxySettings.mode {
|
|
case .none:
|
|
"Downloads ignore configured proxies."
|
|
case .system:
|
|
"Downloads use the matching macOS system proxy when one is configured."
|
|
case .custom:
|
|
if let proxyURI = settings.proxySettings.customProxyURI {
|
|
"Downloads use \(proxyURI)."
|
|
} else {
|
|
"Enter a proxy host and port to enable the custom proxy."
|
|
}
|
|
}
|
|
}
|
|
|
|
private func proxyBinding<Value>(_ keyPath: WritableKeyPath<ProxySettings, Value>) -> Binding<Value> {
|
|
Binding {
|
|
settings.proxySettings[keyPath: keyPath]
|
|
} set: { newValue in
|
|
var proxySettings = settings.proxySettings
|
|
proxySettings[keyPath: keyPath] = newValue
|
|
settings.proxySettings = proxySettings
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct DownloadSettingsPane: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
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)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
}
|
|
|
|
private struct LocationsSettingsPane: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
ForEach(DownloadCategory.allCases, id: \.self) { category in
|
|
DirectoryPickerRow(category: category)
|
|
}
|
|
|
|
HStack {
|
|
Spacer()
|
|
Button("Reset Defaults") {
|
|
settings.resetDirectories()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct DirectoryPickerRow: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
let category: DownloadCategory
|
|
|
|
@State private var path = ""
|
|
|
|
var body: some View {
|
|
HStack(spacing: 10) {
|
|
Label(category.rawValue, systemImage: category.symbolName)
|
|
.frame(width: 125, alignment: .leading)
|
|
|
|
TextField("Folder path", text: Binding(
|
|
get: { settings.downloadDirectories[category] ?? path },
|
|
set: { newValue in
|
|
path = newValue
|
|
settings.setDirectory(newValue, for: category)
|
|
}
|
|
))
|
|
.textFieldStyle(.roundedBorder)
|
|
.font(.system(.body, design: .monospaced))
|
|
|
|
Button {
|
|
selectFolder()
|
|
} label: {
|
|
Label("Select", systemImage: "folder.badge.plus")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func selectFolder() {
|
|
let panel = NSOpenPanel()
|
|
panel.canChooseFiles = false
|
|
panel.canChooseDirectories = true
|
|
panel.allowsMultipleSelection = false
|
|
panel.canCreateDirectories = true
|
|
panel.directoryURL = settings.destinationDirectory(for: category)
|
|
|
|
if panel.runModal() == .OK, let url = panel.url {
|
|
settings.setDirectory(url.path, for: category)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct SiteLoginsSettingsPane: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
@State private var urlPattern = ""
|
|
@State private var username = ""
|
|
@State private var password = ""
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10) {
|
|
GridRow {
|
|
Text("URL Pattern")
|
|
TextField("*.github.com", text: $urlPattern)
|
|
.textFieldStyle(.roundedBorder)
|
|
}
|
|
|
|
GridRow {
|
|
Text("Username")
|
|
TextField("Username", text: $username)
|
|
.textFieldStyle(.roundedBorder)
|
|
}
|
|
|
|
GridRow {
|
|
Text("Password")
|
|
SecureField("Password", text: $password)
|
|
.textFieldStyle(.roundedBorder)
|
|
}
|
|
}
|
|
|
|
HStack {
|
|
Text(settings.message)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
Spacer()
|
|
Button {
|
|
settings.addSiteLogin(urlPattern: urlPattern, username: username, password: password)
|
|
if settings.message.hasPrefix("Added") {
|
|
urlPattern = ""
|
|
username = ""
|
|
password = ""
|
|
}
|
|
} label: {
|
|
Label("Add Login", systemImage: "plus")
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
}
|
|
|
|
List {
|
|
ForEach(settings.siteLogins) { login in
|
|
HStack {
|
|
Image(systemName: "key.horizontal")
|
|
.foregroundStyle(.secondary)
|
|
Text(login.urlPattern)
|
|
.font(.system(.body, design: .monospaced))
|
|
Spacer()
|
|
Text(login.username)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.onDelete(perform: settings.deleteSiteLogins)
|
|
}
|
|
.frame(minHeight: 180)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct PowerSettingsPane: View {
|
|
@EnvironmentObject private var settings: AppSettings
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section {
|
|
Toggle("Prevent system sleep while downloads are active", isOn: $settings.preventsSleepWhileDownloading)
|
|
Text("The display may still turn off. Firelink only keeps macOS awake enough to finish active downloads.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
}
|
|
}
|