Files
Rsync-Manager/RsyncManager/SettingsView.swift
T
2026-09-08 14:12:59 +01:00

141 lines
6.7 KiB
Swift

import SwiftUI
import ServiceManagement
struct SettingsView: View {
@EnvironmentObject private var automation: BackgroundSyncManager
@AppStorage("defaultSSHUsername") private var defaultSSHUsername = NSUserName()
@AppStorage("defaultSSHKeyPath") private var defaultSSHKeyPath = ""
@AppStorage("defaultSSHPort") private var defaultSSHPort = 22
@AppStorage("defaultRemoteHost") private var defaultRemoteHost = ""
@AppStorage("backgroundSchedulesEnabled") private var backgroundSchedulesEnabled = true
@AppStorage("fileWatchingEnabled") private var fileWatchingEnabled = true
@AppStorage("defaultWatchDebounceSeconds") private var defaultWatchDebounceSeconds = 3
@State private var defaultPassword = ""
@State private var passwordSaved = false
@State private var launchAtLogin = false
@State private var launchError: String?
var body: some View {
TabView {
Form {
Section("Default SSH Connection") {
TextField("Username", text: $defaultSSHUsername, prompt: Text(NSUserName()))
TextField("Default host", text: $defaultRemoteHost, prompt: Text("server.local"))
Stepper("SSH port: \(defaultSSHPort)", value: $defaultSSHPort, in: 1...65535)
HStack {
TextField("Private SSH key", text: $defaultSSHKeyPath, prompt: Text("~/.ssh/id_ed25519"))
Button("Choose…") {
if let selected = FilePicker.chooseFile() { defaultSSHKeyPath = selected }
}
}
SecureField("Default password", text: $defaultPassword)
HStack {
Text("Stored securely in the macOS Keychain, never in SwiftData or UserDefaults.")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Button(passwordSaved ? "Saved" : "Save Password") {
KeychainStore.set(defaultPassword, for: "defaultSSHPassword")
passwordSaved = true
}
.disabled(defaultPassword.isEmpty)
}
}
Section("New Jobs") {
Text("New SSH jobs use these values as defaults and can override them independently.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.tabItem { Label("Connections", systemImage: "network") }
Form {
Section("Background Scheduling") {
Toggle("Run scheduled jobs", isOn: $backgroundSchedulesEnabled)
Toggle("Launch Rsync Manager at login", isOn: Binding(
get: { launchAtLogin },
set: { setLaunchAtLogin($0) }
))
HStack {
Label("Configured jobs", systemImage: "calendar.badge.clock")
Spacer()
Text("\(automation.scheduledJobCount)")
.foregroundStyle(.secondary)
}
Text("Scheduled jobs run without opening a modal and are written to History. Rsync Manager continues processing them while its window is hidden, minimised or closed, provided the application itself remains running.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("File Watching") {
Toggle("Watch enabled jobs for changes", isOn: $fileWatchingEnabled)
Stepper("Default debounce: \(defaultWatchDebounceSeconds) seconds", value: $defaultWatchDebounceSeconds, in: 1...60)
HStack {
Label("Watched jobs", systemImage: "eye.fill")
Spacer()
Text("\(automation.watchedJobCount)")
.foregroundStyle(.secondary)
}
Text("Rsync Manager uses macOS FSEvents, so watched trees do not need to be repeatedly scanned. A short debounce combines rapid saves into a single sync.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.tabItem { Label("Automation", systemImage: "calendar.badge.clock") }
Form {
Section("Rsync") {
LabeledContent("Detected executable", value: RsyncLocator.locate())
Text("Rsync Manager prefers Homebrew rsync, then /usr/local/bin/rsync, and finally the version supplied with macOS.")
.font(.caption)
.foregroundStyle(.secondary)
}
Section("Application") {
LabeledContent("Bundle identifier", value: Bundle.main.bundleIdentifier ?? "net.absdev.RsyncManager")
LabeledContent("Version", value: Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0")
LabeledContent("Background service", value: automation.activity.title)
}
}
.formStyle(.grouped)
.tabItem { Label("General", systemImage: "gearshape") }
}
.frame(width: 650, height: 455)
.onAppear {
defaultPassword = KeychainStore.string(for: "defaultSSHPassword")
passwordSaved = !defaultPassword.isEmpty
launchAtLogin = SMAppService.mainApp.status == .enabled
}
.onChange(of: defaultPassword) { _, _ in passwordSaved = false }
.onChange(of: backgroundSchedulesEnabled) { _, _ in automation.refreshConfiguration() }
.onChange(of: fileWatchingEnabled) { _, _ in automation.refreshConfiguration() }
.alert("Launch at Login", isPresented: Binding(
get: { launchError != nil },
set: { if !$0 { launchError = nil } }
)) {
Button("OK", role: .cancel) { launchError = nil }
} message: {
Text(launchError ?? "Unable to change the login item setting.")
}
}
private func setLaunchAtLogin(_ enabled: Bool) {
do {
if enabled {
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
launchAtLogin = SMAppService.mainApp.status == .enabled
} catch {
launchAtLogin = SMAppService.mainApp.status == .enabled
launchError = error.localizedDescription
}
}
}