feat: add contextual actions and recoverable application uninstall

This commit is contained in:
2026-10-12 15:01:15 +02:00
parent 8d8e2278ab
commit 2598f6f1dd
10 changed files with 2361 additions and 12 deletions
+40 -2
View File
@@ -12,6 +12,10 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
private let loginToggle = NSButton(checkboxWithTitle: "Launch at login", target: nil, action: nil)
private let loginStatus = NSTextField(wrappingLabelWithString: "")
private let loginSettings = NSButton(title: "Open Login Items Settings…", target: nil, action: nil)
private let recoveryLabel = NSTextField(wrappingLabelWithString: "")
private let recoveryButton = NSButton(title: "Retry uninstall recovery…", target: nil, action: nil)
private var recoveryReport: UninstallReport?
var retryUninstallRecovery: (() async -> UninstallReport)?
init(shortcut: GlobalShortcut) {
self.shortcut = shortcut
@@ -113,6 +117,13 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
loginSettings.action = #selector(openLoginSettings)
loginSettings.bezelStyle = .rounded
loginStatus.setAccessibilityLabel("Launch at login status")
recoveryLabel.textColor = SunsetPalette.secondaryText
recoveryLabel.isHidden = true
recoveryLabel.setAccessibilityLabel("Uninstall recovery status")
recoveryButton.isHidden = true
recoveryButton.bezelStyle = .rounded
recoveryButton.target = self
recoveryButton.action = #selector(showUninstallRecovery)
heading.font = .systemFont(ofSize: 20, weight: .semibold)
heading.textColor = SunsetPalette.text
@@ -128,7 +139,7 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
let stack = NSStackView(views: [
heading, controls, instructions, statusLabel, errorLabel, spotlight, keyboardSettings,
loginHeading, loginToggle, loginStatus, loginSettings
loginHeading, loginToggle, loginStatus, loginSettings, recoveryLabel, recoveryButton
])
stack.orientation = .vertical
stack.alignment = .leading
@@ -152,11 +163,38 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
stack.bottomAnchor.constraint(lessThanOrEqualTo: content.bottomAnchor, constant: -24),
recorder.widthAnchor.constraint(greaterThanOrEqualToConstant: 190)
])
for label in [instructions, statusLabel, errorLabel, spotlight, loginStatus] {
for label in [instructions, statusLabel, errorLabel, spotlight, loginStatus, recoveryLabel] {
label.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
}
}
func setUninstallRecoveryReport(_ report: UninstallReport) {
recoveryReport = report
recoveryLabel.isHidden = report.messages.isEmpty && !report.needsRecovery
recoveryButton.isHidden = recoveryLabel.isHidden
recoveryLabel.stringValue = report.needsRecovery
? "An interrupted uninstall needs attention. Files have been retained for recovery."
: "Interrupted uninstall recovery completed."
recoveryButton.title = report.needsRecovery ? "Retry uninstall recovery…" : "View recovery details…"
}
@objc private func showUninstallRecovery() {
guard let report = recoveryReport, let window else { return }
recoveryButton.isEnabled = false
Task {
let result: UninstallReport
if report.needsRecovery, let retryUninstallRecovery {
result = await retryUninstallRecovery()
setUninstallRecoveryReport(result)
} else { result = report }
let alert = NSAlert()
alert.messageText = result.needsRecovery ? "Recovery needs attention" : "Recovery complete"
alert.informativeText = result.messages.isEmpty ? "No pending files need recovery." : result.messages.joined(separator: "\n")
await alert.beginSheetModal(for: window)
recoveryButton.isEnabled = true
}
}
private func refreshLogin() {
let status = SMAppService.mainApp.status
loginToggle.state = (status == .enabled || status == .requiresApproval) ? .on : .off