fix: morph launcher into uninstall review and align its filter

This commit is contained in:
2026-10-12 22:17:49 +02:00
parent b69152e2c0
commit 47aa757526
3 changed files with 124 additions and 54 deletions
+57 -43
View File
@@ -2,7 +2,7 @@ import AppKit
import ConjureKit
@MainActor
final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
final class UninstallReviewController: NSViewController, NSSearchFieldDelegate, NSTableViewDataSource, NSTableViewDelegate {
private let application: Application
private let uninstaller: ApplicationUninstaller
private let search = NSSearchField()
@@ -15,31 +15,23 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
private var selected: Set<URL> = []
private var busy = false
private var scanTask: Task<Void, Never>?
private var window: NSWindow? { view.window }
var closed: (() -> Void)?
var changed: (() -> Void)?
init(application: Application, uninstaller: ApplicationUninstaller) {
self.application = application
self.uninstaller = uninstaller
let window = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 750, height: 540),
styleMask: [.titled, .closable, .fullSizeContentView], backing: .buffered, defer: false)
window.title = "Uninstall \(application.name)"
window.appearance = NSAppearance(named: .darkAqua)
window.titlebarAppearsTransparent = true
window.backgroundColor = .clear
window.isOpaque = false
window.isReleasedWhenClosed = false
super.init(window: window)
window.delegate = self
configure(window)
super.init(nibName: nil, bundle: nil)
configure()
}
required init?(coder: NSCoder) { fatalError("Conjure creates uninstall review programmatically") }
func present(on parent: NSWindow) {
func present() {
guard let window else { return }
parent.beginSheet(window)
window.makeFirstResponder(search)
(search.currentEditor() as? NSTextView)?.insertionPointColor = SunsetPalette.accent
scanTask = Task { [weak self, application] in
do {
let found = try await Task.detached(priority: .userInitiated) {
@@ -56,21 +48,22 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
}
}
private func configure(_ window: NSWindow) {
let content = NSVisualEffectView()
content.material = .hudWindow
content.blendingMode = .behindWindow
content.state = .active
window.contentView = content
let background = LauncherBackground(frame: content.bounds)
background.autoresizingMask = [.width, .height]
background.setAccessibilityElement(false)
content.addSubview(background)
search.placeholderString = "Filter files and folders by name or path…"
private func configure() {
let content = UninstallReviewContent()
content.wantsLayer = true
view = content
search.placeholderAttributedString = NSAttributedString(
string: "Filter files and folders by name or path…",
attributes: [.foregroundColor: SunsetPalette.secondaryText, .font: NSFont.systemFont(ofSize: 20)]
)
if let cell = search.cell as? NSSearchFieldCell {
cell.searchButtonCell = nil
cell.cancelButtonCell = nil
}
search.delegate = self
search.sendsSearchStringImmediately = true
search.setAccessibilityLabel("Filter uninstall files")
search.font = .systemFont(ofSize: 18)
search.font = .systemFont(ofSize: 20)
search.textColor = SunsetPalette.text
search.isBordered = false
search.drawsBackground = false
@@ -80,7 +73,16 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
status.setAccessibilityLabel("Uninstall selection and progress")
back.target = self
back.action = #selector(goBack)
back.bezelStyle = .rounded
back.title = ""
back.image = NSImage(systemSymbolName: "chevron.left", accessibilityDescription: nil)
back.imageScaling = .scaleProportionallyDown
back.isBordered = false
back.contentTintColor = SunsetPalette.secondaryText
back.setAccessibilityLabel("Back to applications")
let header = NSStackView(views: [back, search])
header.orientation = .horizontal
header.alignment = .centerY
header.spacing = 12
remove.target = self
remove.action = #selector(confirmUninstall)
remove.contentTintColor = .systemRed
@@ -105,18 +107,21 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
scroll.autohidesScrollers = true
scroll.drawsBackground = false
scroll.contentView.drawsBackground = false
let footer = NSStackView(views: [back, NSView(), remove])
let footer = NSStackView(views: [NSView(), remove])
footer.orientation = .horizontal
footer.spacing = 12
for view in [search, status, scroll, footer] {
for view in [header, status, scroll, footer] {
view.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(view)
}
NSLayoutConstraint.activate([
search.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
search.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
search.topAnchor.constraint(equalTo: (window.contentLayoutGuide as! NSLayoutGuide).topAnchor, constant: 16),
search.heightAnchor.constraint(equalToConstant: 32),
header.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 16),
header.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -24),
header.topAnchor.constraint(equalTo: content.topAnchor, constant: 20),
header.heightAnchor.constraint(equalToConstant: 32),
back.widthAnchor.constraint(equalToConstant: 22),
back.heightAnchor.constraint(equalToConstant: 22),
search.widthAnchor.constraint(equalTo: header.widthAnchor, constant: -34),
status.leadingAnchor.constraint(equalTo: search.leadingAnchor),
status.trailingAnchor.constraint(equalTo: search.trailingAnchor),
status.topAnchor.constraint(equalTo: search.bottomAnchor, constant: 12),
@@ -161,7 +166,7 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
return true
}
private func moveSelection(_ offset: Int) {
func moveSelection(_ offset: Int) {
guard !visible.isEmpty else { return }
let row = min(max(table.selectedRow + offset, 0), visible.count - 1)
table.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: false)
@@ -300,23 +305,32 @@ final class UninstallReviewController: NSWindowController, NSWindowDelegate, NSS
back.isEnabled = !value
remove.isEnabled = !value && !selected.isEmpty
table.reloadData()
if !value { updateStatus() }
if !value {
updateStatus()
window?.makeFirstResponder(search)
}
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
if !busy { goBack() }
return false
}
override func cancelOperation(_ sender: Any?) { goBack() }
@objc private func goBack() {
guard !busy, let window, let parent = window.sheetParent else { return }
@objc func goBack() {
guard !busy else { return }
scanTask?.cancel()
parent.endSheet(window)
window.orderOut(nil)
(view as! UninstallReviewContent).acceptsInteraction = false
view.setAccessibilityHidden(true)
closed?()
}
}
@MainActor
private final class UninstallReviewContent: NSView {
var acceptsInteraction = true
override func hitTest(_ point: NSPoint) -> NSView? {
acceptsInteraction ? super.hitTest(point) : nil
}
}
private struct ReviewError: LocalizedError {
let errorDescription: String?
init(_ message: String) { errorDescription = message }