From 7c8c804dacd1747101c0949a3ca26c94c690af79 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 20 May 2026 02:35:59 +0800
Subject: [PATCH] Release TagLauncher 7.5.1
---
Apptag/ContentView.swift | 317 +++++++++++++++++++++++++++++++++++++++++++++++-----
1 files changed, 286 insertions(+), 31 deletions(-)
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index b9bb245..8e1cf0e 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -337,6 +337,7 @@
struct ContentView: View {
let hideOverlay: () -> Void
+ private let initialQuickSearchSource: String?
@State private var allApps: [AppInfo] = []
@State private var tagColors: [String: Int] = [:]
@@ -377,6 +378,27 @@
@State private var bubbleDraftNote = ""
@FocusState private var bubbleNoteFocused: Bool
+ // Quick Search
+ @State private var quickSearchVisible = false
+ @State private var quickSearchQuery = ""
+ @State private var quickSearchDocuments: [QuickSearchDocument] = []
+ @State private var quickSearchResults: [QuickSearchResult] = []
+ @State private var quickSearchSelectedID: URL? = nil
+ @State private var quickSearchManualSelection = false
+ @State private var quickSearchFocusToken = 0
+ @State private var quickSearchCloseHidesOverlay = false
+ @State private var quickSearchErrorMessage: String? = nil
+ @State private var initialQuickSearchConsumed = false
+
+ init(hideOverlay: @escaping () -> Void, initialQuickSearchSource: String? = nil) {
+ self.hideOverlay = hideOverlay
+ self.initialQuickSearchSource = initialQuickSearchSource
+ let startsInQuickSearch = initialQuickSearchSource != nil
+ _quickSearchVisible = State(initialValue: startsInQuickSearch)
+ _quickSearchCloseHidesOverlay = State(initialValue: initialQuickSearchSource == QuickSearchOpenSource.globalHidden)
+ _quickSearchFocusToken = State(initialValue: startsInQuickSearch ? 1 : 0)
+ }
+
// Configurable defaults
@AppStorage("defaultGroupName") private var defaultGroupName = "Other"
@AppStorage("tagFontSize") private var tagFontSize: Double = AppDefaults.tagFontSize
@@ -411,37 +433,47 @@
displayMode == "coloredContainer" || displayMode == "coloredGridContainer"
}
+ private var quickSearchOnlyMode: Bool {
+ quickSearchVisible && quickSearchCloseHidesOverlay
+ }
+
var body: some View {
ZStack {
- VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
- .ignoresSafeArea()
- .allowsHitTesting(false)
+ if !quickSearchOnlyMode {
+ VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
+ .ignoresSafeArea()
+ .allowsHitTesting(false)
+ }
- if notchHeight > 0 {
- VStack {
- Rectangle().fill(.black)
- .frame(height: notchHeight)
- .ignoresSafeArea(edges: .top)
- Spacer()
+ if !quickSearchOnlyMode {
+ if notchHeight > 0 {
+ VStack {
+ Rectangle().fill(.black)
+ .frame(height: notchHeight)
+ .ignoresSafeArea(edges: .top)
+ Spacer()
+ }
+ .allowsHitTesting(false)
}
- .allowsHitTesting(false)
+
+ switch editPhase {
+ case .none:
+ normalContent
+ case .editingTags:
+ editTagsView
+ case .editingApps:
+ editAppsView
+ }
+
+ uncommonAppBubbleOverlay
+ smartStartNoticeOverlay
+ editActionFeedbackOverlay
+ uncategorizedDropConfirmOverlay
}
- switch editPhase {
- case .none:
- normalContent
- case .editingTags:
- editTagsView
- case .editingApps:
- editAppsView
- }
+ quickSearchOverlay
- uncommonAppBubbleOverlay
- smartStartNoticeOverlay
- editActionFeedbackOverlay
- uncategorizedDropConfirmOverlay
-
- if let message = dropWarningToast {
+ if !quickSearchOnlyMode, let message = dropWarningToast {
Text(message)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(.primary)
@@ -456,7 +488,7 @@
.allowsHitTesting(false)
}
- if dropRefreshVisible {
+ if !quickSearchOnlyMode && dropRefreshVisible {
Color.black.opacity(0.08)
.ignoresSafeArea()
.transition(.opacity)
@@ -479,6 +511,17 @@
.onAppear {
refreshNotchHeight()
refreshApps()
+ if let initialQuickSearchSource, !initialQuickSearchConsumed {
+ initialQuickSearchConsumed = true
+ quickSearchCloseHidesOverlay = initialQuickSearchSource == QuickSearchOpenSource.globalHidden
+ quickSearchVisible = true
+ quickSearchFocusToken &+= 1
+ NotificationCenter.default.post(
+ name: .tagLauncherQuickSearchVisibilityChanged,
+ object: nil,
+ userInfo: ["active": true]
+ )
+ }
}
.onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
resetTransientDragState()
@@ -487,6 +530,14 @@
}
.onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
resetTransientDragState()
+ closeQuickSearch(notify: true, hideOverlayIfNeeded: false)
+ }
+ .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchRequested)) { notification in
+ let source = notification.userInfo?["source"] as? String ?? QuickSearchOpenSource.mainOverlay
+ openQuickSearch(source: source)
+ }
+ .onReceive(NotificationCenter.default.publisher(for: .tagLauncherQuickSearchDismissRequested)) { _ in
+ closeQuickSearch()
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard allApps.isEmpty, !refreshInProgress else { return }
@@ -517,6 +568,10 @@
if editingBubble != nil && !focused {
commitBubbleNote()
}
+ }
+ .onChange(of: quickSearchQuery) { _, _ in
+ quickSearchErrorMessage = nil
+ refreshQuickSearchResults()
}
.onChange(of: pendingUncategorizedDrop != nil) { _, active in
NotificationCenter.default.post(
@@ -551,6 +606,179 @@
NotificationCenter.default.post(name: .tagLauncherEditModeChanged, object: nil, userInfo: ["active": false])
}
}
+ }
+
+ // MARK: - Quick Search
+
+ private var quickSearchOverlay: some View {
+ GeometryReader { proxy in
+ if quickSearchVisible {
+ QuickSearchBackdropClickView {
+ closeQuickSearch()
+ }
+ .frame(width: proxy.size.width, height: proxy.size.height)
+ .ignoresSafeArea()
+ .zIndex(899)
+
+ QuickSearchOverlayView(
+ query: $quickSearchQuery,
+ results: quickSearchResults,
+ selectedID: quickSearchSelectedID,
+ focusToken: quickSearchFocusToken,
+ isLoading: quickSearchDocuments.isEmpty && refreshInProgress,
+ maxVisibleRows: quickSearchMaxVisibleRows(in: proxy.size),
+ errorMessage: quickSearchErrorMessage,
+ onCommand: handleQuickSearchCommand,
+ onHover: selectQuickSearchResult,
+ onLaunch: launchQuickSearchResult
+ )
+ .position(
+ x: proxy.size.width / 2,
+ y: quickSearchPanelCenterY(in: proxy.size)
+ )
+ .transition(.scale(scale: 0.98).combined(with: .opacity))
+ .zIndex(900)
+ }
+ }
+ .ignoresSafeArea()
+ .animation(.easeOut(duration: 0.12), value: quickSearchVisible)
+ }
+
+ private func quickSearchPanelCenterY(in size: CGSize) -> CGFloat {
+ let visibleRows = max(1, min(quickSearchResults.isEmpty ? 1 : quickSearchResults.count, quickSearchMaxVisibleRows(in: size)))
+ let estimatedPanelHeight = CGFloat(visibleRows) * 76 + 122
+ return quickSearchPanelTopY(in: size) + estimatedPanelHeight / 2
+ }
+
+ private func quickSearchPanelTopY(in size: CGSize) -> CGFloat {
+ max(notchHeight + 54, min(112, size.height * 0.12))
+ }
+
+ private func quickSearchMaxVisibleRows(in size: CGSize) -> Int {
+ let bottomClearance: CGFloat = 84
+ let chromeHeight: CGFloat = 122
+ let rowHeightWithSpacing: CGFloat = 76
+ let availableHeight = max(0, size.height - quickSearchPanelTopY(in: size) - bottomClearance - chromeHeight)
+ return max(1, min(8, Int(floor(availableHeight / rowHeightWithSpacing))))
+ }
+
+ private func openQuickSearch(source: String) {
+ guard canOpenQuickSearch else { return }
+ dismissAppBubble()
+ quickSearchCloseHidesOverlay = source == QuickSearchOpenSource.globalHidden
+ quickSearchVisible = true
+ quickSearchQuery = ""
+ quickSearchManualSelection = false
+ quickSearchErrorMessage = nil
+ quickSearchFocusToken &+= 1
+ refreshQuickSearchResults()
+ NotificationCenter.default.post(
+ name: .tagLauncherQuickSearchVisibilityChanged,
+ object: nil,
+ userInfo: ["active": true]
+ )
+ }
+
+ private var canOpenQuickSearch: Bool {
+ editPhase == .none
+ && pendingUncategorizedDrop == nil
+ && smartStartNotice == nil
+ && !dropRefreshVisible
+ && !quickSearchVisible
+ }
+
+ private func closeQuickSearch(notify: Bool = true, hideOverlayIfNeeded: Bool = true) {
+ let shouldHideOverlay = quickSearchVisible && quickSearchCloseHidesOverlay && hideOverlayIfNeeded
+ guard quickSearchVisible else { return }
+ quickSearchVisible = false
+ quickSearchQuery = ""
+ quickSearchResults = []
+ quickSearchSelectedID = nil
+ quickSearchManualSelection = false
+ quickSearchErrorMessage = nil
+ quickSearchCloseHidesOverlay = false
+ if notify {
+ NotificationCenter.default.post(
+ name: .tagLauncherQuickSearchVisibilityChanged,
+ object: nil,
+ userInfo: ["active": false]
+ )
+ }
+ if shouldHideOverlay {
+ hideOverlay()
+ }
+ }
+
+ private func refreshQuickSearchResults() {
+ let previousSelection = quickSearchSelectedID
+ quickSearchResults = QuickSearchEngine.search(quickSearchQuery, documents: quickSearchDocuments)
+
+ if quickSearchResults.isEmpty {
+ quickSearchSelectedID = nil
+ quickSearchManualSelection = false
+ return
+ }
+
+ if quickSearchManualSelection,
+ let previousSelection,
+ quickSearchResults.contains(where: { $0.id == previousSelection }) {
+ quickSearchSelectedID = previousSelection
+ } else {
+ quickSearchSelectedID = quickSearchResults.first?.id
+ quickSearchManualSelection = false
+ }
+ }
+
+ private func handleQuickSearchCommand(_ command: QuickSearchCommand) {
+ switch command {
+ case .moveUp:
+ moveQuickSearchSelection(by: -1)
+ case .moveDown:
+ moveQuickSearchSelection(by: 1)
+ case .submit:
+ guard let selected = selectedQuickSearchResult else { return }
+ launchQuickSearchResult(selected)
+ case .dismiss:
+ closeQuickSearch()
+ }
+ }
+
+ private var selectedQuickSearchResult: QuickSearchResult? {
+ guard let quickSearchSelectedID else { return nil }
+ return quickSearchResults.first { $0.id == quickSearchSelectedID }
+ }
+
+ private func moveQuickSearchSelection(by delta: Int) {
+ guard !quickSearchResults.isEmpty else { return }
+ let currentIndex = quickSearchSelectedID.flatMap { id in
+ quickSearchResults.firstIndex { $0.id == id }
+ } ?? 0
+ let nextIndex = min(max(currentIndex + delta, 0), quickSearchResults.count - 1)
+ quickSearchSelectedID = quickSearchResults[nextIndex].id
+ quickSearchManualSelection = true
+ }
+
+ private func selectQuickSearchResult(_ result: QuickSearchResult) {
+ quickSearchSelectedID = result.id
+ quickSearchManualSelection = true
+ }
+
+ private func launchQuickSearchResult(_ result: QuickSearchResult) {
+ quickSearchErrorMessage = nil
+ launchApp(
+ result.app,
+ closeQuickSearchOnSuccess: true,
+ closeOverlayOnSuccess: true,
+ onFailure: {
+ quickSearchErrorMessage = tr("quickSearch.launchFailed")
+ quickSearchFocusToken &+= 1
+ NSAccessibility.post(
+ element: NSApp.mainWindow as Any,
+ notification: .announcementRequested,
+ userInfo: [.announcement: tr("quickSearch.launchFailed")]
+ )
+ }
+ )
}
// MARK: - Normal Content
@@ -2304,12 +2532,17 @@
)
let store = smartStartResult.store
let apps = TagEditor.annotate(apps: scannedApps, store: store)
+ let quickSearchDocs = QuickSearchEngine.makeDocuments(apps: apps, store: store)
let colors = store.tags.mapValues { $0.color }
let order = TagEditor.orderedTagNames()
DispatchQueue.main.async {
allApps = apps
+ quickSearchDocuments = quickSearchDocs
tagColors = colors
draggedTagNames = order
+ if quickSearchVisible {
+ refreshQuickSearchResults()
+ }
handleSmartStartRunResult(smartStartResult)
if forceLayoutRefresh {
finishDropRefreshAfterMinimumDuration()
@@ -2325,15 +2558,37 @@
}
}
- func openApp(_ app: AppInfo) {
+ private func launchApp(
+ _ app: AppInfo,
+ closeQuickSearchOnSuccess: Bool = false,
+ closeOverlayOnSuccess: Bool = true,
+ onFailure: (() -> Void)? = nil
+ ) {
appDragModeActive = false
endTagNavReorder()
- hideOverlay()
- DispatchQueue.global(qos: .utility).async {
- TagEditor.recordLauncherOpen(for: app.path.path)
- }
let configuration = NSWorkspace.OpenConfiguration()
- NSWorkspace.shared.openApplication(at: app.path, configuration: configuration)
+ NSWorkspace.shared.openApplication(at: app.path, configuration: configuration) { _, error in
+ DispatchQueue.main.async {
+ guard error == nil else {
+ onFailure?()
+ return
+ }
+
+ DispatchQueue.global(qos: .utility).async {
+ TagEditor.recordLauncherOpen(for: app.path.path)
+ }
+ if closeQuickSearchOnSuccess {
+ closeQuickSearch(hideOverlayIfNeeded: false)
+ }
+ if closeOverlayOnSuccess {
+ hideOverlay()
+ }
+ }
+ }
+ }
+
+ func openApp(_ app: AppInfo) {
+ launchApp(app)
}
}
--
Gitblit v1.9.3