feat: add favorites with match-quality-aware ranking
This commit is contained in:
@@ -6,6 +6,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
private let search = ActionsSearchField()
|
||||
private let entries = [
|
||||
ActionEntry(title: "Open", symbol: "arrow.up.forward.app", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Favorite", symbol: "star", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Show in Finder", symbol: "finder", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Reset ranking", symbol: "arrow.counterclockwise", color: SunsetPalette.text),
|
||||
ActionEntry(title: "Quit application", symbol: "stop.circle", color: SunsetPalette.text),
|
||||
@@ -24,6 +25,8 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
private var transitionGeneration = 0
|
||||
private var panel: ActionsPanelView { view as! ActionsPanelView }
|
||||
var open: (() -> Void)?
|
||||
var toggleFavorite: (() -> Void)?
|
||||
var isFavorite = false
|
||||
var showInFinder: (() -> Void)?
|
||||
var resetRanking: (() -> Void)?
|
||||
var quit: (() -> Void)?
|
||||
@@ -126,6 +129,13 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
let pill = anchor.superview ?? anchor
|
||||
_ = view
|
||||
applicationLabel.stringValue = application.name
|
||||
let favorite = entries[1]
|
||||
let favoriteTitle = isFavorite ? "Remove Favorite" : "Favorite"
|
||||
favorite.button.attributedTitle = NSAttributedString(string: favoriteTitle, attributes: [
|
||||
.foregroundColor: SunsetPalette.text, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
])
|
||||
favorite.button.setAccessibilityLabel(favoriteTitle)
|
||||
favorite.icon.image = NSImage(systemSymbolName: isFavorite ? "star.fill" : "star", accessibilityDescription: nil)
|
||||
search.stringValue = ""
|
||||
controlTextDidChange(Notification(name: NSControl.textDidChangeNotification))
|
||||
transitionGeneration += 1
|
||||
@@ -242,10 +252,11 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
close()
|
||||
switch index {
|
||||
case 0: open?()
|
||||
case 1: showInFinder?()
|
||||
case 2: resetRanking?()
|
||||
case 3: quit?()
|
||||
case 4: uninstall?()
|
||||
case 1: toggleFavorite?()
|
||||
case 2: showInFinder?()
|
||||
case 3: resetRanking?()
|
||||
case 4: quit?()
|
||||
case 5: uninstall?()
|
||||
default: break
|
||||
}
|
||||
}
|
||||
@@ -255,6 +266,7 @@ final class ApplicationActionsController: NSViewController, NSSearchFieldDelegat
|
||||
private struct ActionEntry {
|
||||
let view = LauncherRowView()
|
||||
let button: NSButton
|
||||
let icon = ActionIconView()
|
||||
let returnKey = ShortcutIndicator("↵")
|
||||
|
||||
init(title: String, symbol: String, color: NSColor) {
|
||||
@@ -267,7 +279,6 @@ private struct ActionEntry {
|
||||
.foregroundColor: color, .font: NSFont.systemFont(ofSize: LauncherRowView.textSize, weight: .medium)
|
||||
])
|
||||
button.setAccessibilityLabel(title)
|
||||
let icon = ActionIconView()
|
||||
icon.image = NSImage(systemSymbolName: symbol, accessibilityDescription: nil)
|
||||
?? (symbol == "finder" ? NSWorkspace.shared.icon(forFile: "/System/Library/CoreServices/Finder.app") : nil)
|
||||
icon.contentTintColor = color
|
||||
|
||||
@@ -591,6 +591,12 @@ final class LauncherWindowController: NSWindowController, NSWindowDelegate, NSSe
|
||||
(window as? LauncherPanel)?.resetNumberHints()
|
||||
let application = results[tableView.selectedRow]
|
||||
actions.open = { [weak self] in self?.openApplication(application) }
|
||||
actions.isFavorite = history.isFavorite(application)
|
||||
actions.toggleFavorite = { [weak self] in
|
||||
guard let self else { return }
|
||||
self.history.toggleFavorite(application)
|
||||
self.updateResults(preservingSelection: true)
|
||||
}
|
||||
actions.showInFinder = { [weak self] in
|
||||
self?.dismiss(restoringFocus: false)
|
||||
NSWorkspace.shared.activateFileViewerSelecting([application.url])
|
||||
|
||||
@@ -8,9 +8,11 @@ public final class LaunchHistory {
|
||||
}
|
||||
|
||||
private static let storageKey = "applicationLaunchHistory"
|
||||
private static let favoritesStorageKey = "applicationFavorites"
|
||||
private let defaults: UserDefaults
|
||||
private let now: () -> Date
|
||||
private var usage: [URL: Usage] = [:]
|
||||
private var favorites = Set<URL>()
|
||||
|
||||
public init(defaults: UserDefaults = .standard, now: @escaping () -> Date = Date.init) {
|
||||
self.defaults = defaults
|
||||
@@ -23,6 +25,21 @@ public final class LaunchHistory {
|
||||
lastLaunch.timeIntervalSinceReferenceDate.isFinite else { continue }
|
||||
usage[url] = Usage(count: count, lastLaunch: lastLaunch)
|
||||
}
|
||||
for identifier in defaults.stringArray(forKey: Self.favoritesStorageKey) ?? [] {
|
||||
guard let url = URL(string: identifier), url.isFileURL else { continue }
|
||||
favorites.insert(url)
|
||||
}
|
||||
}
|
||||
|
||||
public func isFavorite(_ application: Application) -> Bool {
|
||||
favorites.contains(application.id)
|
||||
}
|
||||
|
||||
public func toggleFavorite(_ application: Application) {
|
||||
if favorites.remove(application.id) == nil {
|
||||
favorites.insert(application.id)
|
||||
}
|
||||
defaults.set(favorites.map(\.absoluteString), forKey: Self.favoritesStorageKey)
|
||||
}
|
||||
|
||||
public func recordLaunch(of application: Application) {
|
||||
@@ -57,6 +74,9 @@ public final class LaunchHistory {
|
||||
}
|
||||
|
||||
private func preferred(_ lhs: Application, _ rhs: Application) -> Bool {
|
||||
let leftFavorite = isFavorite(lhs)
|
||||
let rightFavorite = isFavorite(rhs)
|
||||
if leftFavorite != rightFavorite { return leftFavorite }
|
||||
let left = usage[lhs.id]
|
||||
let right = usage[rhs.id]
|
||||
let leftCount = left?.count ?? 0
|
||||
|
||||
Reference in New Issue
Block a user