From 0badb998147135c75187a0c48f926b665af734c5 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Sun, 28 Jun 2026 02:53:51 +0800
Subject: [PATCH] Release 8.1.6 build 20260628.0230
---
src/Apptag/Localization/fr.json | 4
src/Apptag/Localization/ru.json | 4
src/Apptag/Localization/ar.json | 4
src/Apptag/Localization/ro.json | 4
src/Apptag/Localization/en.json | 4
src/Apptag/Localization/sv.json | 4
src/Apptag/Localization/zh-Hant.json | 4
src/Apptag/ApptagApp.swift | 173 ++++
src/Apptag/Localization/pl.json | 4
src/Apptag/Localization/es.json | 4
src/Apptag/Localization/th.json | 4
src/Apptag/Localization/nn.json | 4
src/Apptag/ProEntitlement.swift | 4
src/CHANGELOG.md | 21
src/Apptag/Info.plist | 15
src/Apptag/Localization/cs.json | 4
src/Apptag/Localization/ms.json | 4
src/Apptag/Localization/it.json | 4
src/Apptag/Localization/tr.json | 4
src/Scripts/pro_feature_gate_qa.sh | 7
src/Apptag/Localization/da.json | 4
src/Apptag/Localization/id.json | 4
src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md | 606 +++++++++++++++++
src/Scripts/smartstart_default_categories_qa.sh | 8
src/Scripts/window_logic_qa.sh | 308 ++++++--
src/Apptag/Localization/ja.json | 4
src/Apptag/PreferencesView.swift | 56 +
CODE_GOVERNANCE.md | 2
src/Apptag/Localization/de.json | 4
src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md | 376 +++++++++++
src/Apptag/Localization/pt-BR.json | 4
CODEGRAPH.md | 17
src/Apptag/Localization/ar-Najdi.json | 4
src/Apptag/Localization/nb.json | 4
src/Scripts/pro_localization_qa.sh | 1
src/Apptag/SmartCategorization/SmartCategory.swift | 3
src/Scripts/url_scheme_activation_qa.sh | 134 +++
src/Apptag/Localization/ko.json | 4
src/Apptag/Localization/no.json | 4
src/Scripts/pro_theme_preview_countdown_qa.sh | 107 +++
src/Apptag/Localization/sr-Cyrl.json | 4
src/Apptag/Localization/zh-Hans.json | 4
src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md | 46
src/Apptag/Localization/nl.json | 4
src/Apptag/Localization/uk.json | 4
src/Apptag/Localization/vi.json | 4
46 files changed, 1,868 insertions(+), 132 deletions(-)
diff --git a/CODEGRAPH.md b/CODEGRAPH.md
index b03aae7..60ce67c 100644
--- a/CODEGRAPH.md
+++ b/CODEGRAPH.md
@@ -19,6 +19,7 @@
- `src/Apptag/ApptagApp.swift`
- `TagLauncherApp`:SwiftUI App 入口。
- `AppDelegate`:菜单栏状态项、Dock 显示策略、热键注册、窗口打开、Quick Search 触发、App 生命周期。
+ - 支持外部 URL 唤起 `taglauncher://show`:只做 AppGrid show/focus,不做 toggle;冷启动时请求会排队到初始化完成后执行;不接管原生触控板手势。
- 支持从标签导航双击进入设置页的 Tags tab。
- 启动时初始化 `ProEntitlementCenter`,保证免费 / Pro / 老用户自动 Pro 状态进入全局同步快照。
- 菜单栏下拉菜单包含免费 / Pro 当前身份状态项;免费状态点击进入 About 页的 Pro 卡片,Pro 状态使用金色皇冠图标。
@@ -34,6 +35,7 @@
- `src/Apptag/PreferencesView.swift`
- `PreferencesView`:设置窗口、语言、通用、主题、快捷键、标签、数据、关于等设置页。
- Theme tab 只写视觉偏好 `appGridThemeID`,不得改变标签、排序、备注或分类数据。
+ - 免费用户预览 Pro 主题时,Theme tab 状态行和当前预览主题卡片显示基于 `ProThemePreviewState.endsAt` 推导的 `MM:SS` 倒计时;倒计时到期只退出预览,不弹阻塞提示。
- Pro 升级 / 权益卡位于 About tab;Data tab 只承载分类方案、导入导出和气泡显示位置等数据管理入口。
## AppGrid 显示与交互
@@ -95,8 +97,8 @@
- `src/Apptag/SmartCategorization/SmartCategory.swift`
- 内置智能分类定义。
- - `SmartCategoryDefaults.orderedIDs` 是 SmartStart 默认初始化标签事实源;当前默认方案为 13 个标签。
- - `SmartCategoryID.smartStartDefaultCategoryID` 负责把旧细分 catalog 分类归并到 13 个默认标签;`finance` 和 `other` 返回 nil,回到未分类;这是新 SmartStart 写入前的归并,不做老用户旧标签自动迁移。
+ - `SmartCategoryDefaults.orderedIDs` 是 SmartStart 默认初始化标签事实源;当前默认方案为 12 个标签。
+ - `SmartCategoryID.smartStartDefaultCategoryID` 负责把旧细分 catalog 分类归并到 12 个默认标签;`Meeting` 归入 `communication`,`finance` 和 `other` 返回 nil,回到未分类;这是新 SmartStart 写入前的归并,不做老用户旧标签自动迁移。
- `src/Apptag/SmartCategorization/SmartCategorizationDraft.swift`
- 智能分类草稿、默认备注 provenance、未分配应用和 warning 模型。
- `src/Apptag/SmartCategorization/SmartStartService.swift`
@@ -121,7 +123,7 @@
## 构建与发布
- `src/Apptag/Info.plist`
- - App 版本、build、bundle metadata。
+ - App 版本、build、bundle metadata;注册公开 URL Scheme `taglauncher`,首期路由只支持 `taglauncher://show`。
- `src/Apptag/TagLauncher.entitlements`
- App sandbox / entitlement 配置。
- `src/build.sh`
@@ -148,7 +150,7 @@
- `src/Scripts/smart_category_localization_qa.sh`
- SmartCategory/default system tag 初始化标签 29 语种本地化检查;禁止非英文语言包直接复制英文标签,保留 `PDF`、`DevOps` 等技术通用词例外。
- `src/Scripts/smartstart_default_categories_qa.sh`
- - SmartStart 默认初始化标签检查;验证 13 个默认分类、catalog 分类归并入口、`finance` / `other` 回未分类、首次 starter tags 和关键多语言目标文案。
+ - SmartStart 默认初始化标签检查;验证 12 个默认分类、catalog 分类归并入口、`Meeting` 归入 `communication`、`finance` / `other` 回未分类、首次 starter tags 和关键多语言目标文案。
- `src/Scripts/quick_search_app_name_qa.sh`
- Quick Search 应用显示名回归。
- `src/Scripts/quick_search_system_app_qa.sh`
@@ -163,14 +165,18 @@
- Pro 文案 29 语种静态门禁;检查 `pro.*` 键完整、占位符完整、无生成污染和非英文语言无英文兜底复制。
- `src/Scripts/pro_feature_gate_qa.sh`
- Pro 功能源码门禁;检查 StoreKit 权益、老用户自动 Pro、主题门禁、导入导出硬拦截、备注额度、排序预览和 Pro UI 提示。
+- `src/Scripts/pro_theme_preview_countdown_qa.sh`
+ - Pro 主题预览倒计时门禁;检查状态行倒计时、当前预览卡片 `MM:SS`、到期清理、29 语种 `%time%` 占位符和禁止写死体验时长。
- `src/Scripts/appgrid_startup_loading_qa.sh`
- AppGrid 快照复用和延迟转圈回归。
+- `src/Scripts/url_scheme_activation_qa.sh`
+ - 外部 URL 唤起门禁;检查 `taglauncher://show` 注册、AppDelegate show/focus 语义、冷启动 pending、29 语种设置文案和禁止私有触控板捕获。
- `src/Scripts/usage_tips_qa.sh`
- AppGrid 使用技巧 AppKit 横幅、主题可读性、防穿透、关闭按钮、29 语种文案回归。
- `src/Scripts/usage_tips_close_reminder_qa.sh`
- 使用技巧关闭提醒、modal 防穿透、设置页预览、29 语种文案回归。
- `src/Scripts/window_logic_qa.sh`
- - 窗口层级、显示隐藏、Dock/菜单栏相关逻辑回归。
+ - 窗口层级、显示隐藏、Dock/菜单栏相关逻辑回归;Dock 项断言按当前 QA 构建路径过滤,避免本机固定正式版 App 时误判重复图标。
- `src/Scripts/app_ordering_data_qa.sh`
- App 排序数据持久化回归。
@@ -212,3 +218,4 @@
- 改 `DataLayer.swift` 通常会影响应用扫描、显示名、标签数据、导入导出和迁移,必须补数据 QA。
- 改 `PreferencesView.swift` 通常需要检查 29 个本地化 JSON、设置持久化和 macOS 14 UI 表现。
- 改 `ApptagApp.swift` 或 `OverlayWindowController.swift` 必须回归菜单栏、Dock 图标策略、热键、窗口层级、全屏/Split View 行为。
+- 改外部 URL 唤起、`Info.plist` URL Scheme 或第三方手势说明时,必须运行 `url_scheme_activation_qa.sh`,并确认不新增 Accessibility/Input Monitoring/private multitouch 依赖。
diff --git a/CODE_GOVERNANCE.md b/CODE_GOVERNANCE.md
index 623718f..553d6ed 100644
--- a/CODE_GOVERNANCE.md
+++ b/CODE_GOVERNANCE.md
@@ -74,6 +74,8 @@
```
- tag、`Info.plist`、`CHANGELOG.md`、`Release/` 资料、最终安装包文件名和 build 编号必须一致。
+- 生成 App Store 发布资料时,App Store Connect 的 What's New 不得只依据目标版本 release scope 或目标版本 changelog 小节。必须先确认当前 App Store 线上版本或当前待替换版本,并从 `CHANGELOG.md` 提取当前商店版本之后到目标版本之间的累计用户可感知变更。
+- 每个 App Store release checklist 必须包含可勾选的 What's New 累计变更门禁:当前商店基线版本、目标版本、累计 changelog 范围、中间版本用户可感知变更、排除原因,以及中英文 What's New 用户审核结果。
## 6. Worktree 使用规则
diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index bb6b32b..53dfb40 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -39,7 +39,10 @@
private static let downloadHelpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherDownloadHelpMenuItem")
private static let externalActivationNotification = Notification.Name("TagLauncherExternalActivationRequested")
private static let externalActivationObject = AppIdentity.bundleIdentifier
+ private static let externalInvocationScheme = "taglauncher"
+ private static let externalInvocationShowHost = "show"
private static let duplicateLaunchSuppressReopenKey = "duplicateLaunchSuppressReopenAt"
+ private static let overlaySpaceHotkeyID: UInt32 = 3
private static let launcherOverlayLevel = NSWindow.Level(rawValue: NSWindow.Level.mainMenu.rawValue - 1)
private static let overlayDefaultLevel = launcherOverlayLevel
private static let overlayTextInputLevel = launcherOverlayLevel
@@ -50,8 +53,10 @@
private var quickSearchLocalMouseMonitor: Any?
private var quickSearchExternalMouseMonitor: Any?
private var settingsWindow: NSWindow? // Track Settings window to keep it above overlay
+ private var explicitPreferencesOpenRequestedAt = Date.distantPast
private var mainHotkeyRef: EventHotKeyRef?
private var quickSearchHotkeyRef: EventHotKeyRef?
+ private var overlaySpaceHotkeyRef: EventHotKeyRef?
private var hotkeyEventHandlerInstalled = false
private var isQuickSearchOpen = false
private var quickSearchShouldHideOverlayOnClose = false
@@ -65,6 +70,8 @@
private var statusMenuScreenForNextOverlay: NSScreen?
private var suppressReopenUntil = Date.distantPast
private var overlayOpenedByQuickSearchOnly = false
+ private var didFinishLaunching = false
+ private var pendingShowOverlayInvocation = false
private lazy var overlayController = OverlayWindowController(
dependencies: OverlayWindowController.Dependencies(
@@ -127,14 +134,16 @@
refreshChromeState: { [weak self] activate, avoidSpaceSwitch in
self?.refreshLauncherChromeState(activate: activate, avoidSpaceSwitch: avoidSpaceSwitch)
},
- onWillHide: {
+ onWillHide: { [weak self] in
+ self?.unregisterOverlaySpaceHotkey()
TagDatabase.flushPendingCategorySchemeBackupBatch()
},
onDidHide: { [weak self] in
self?.overlayOpenedByQuickSearchOnly = false
NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
},
- onDidShow: {
+ onDidShow: { [weak self] in
+ self?.refreshOverlaySpaceHotkeyRegistration()
NotificationCenter.default.post(name: .tagLauncherOverlayDidShow, object: nil)
}
)
@@ -213,6 +222,9 @@
observeLanguageChanges()
setupLaunchAtLogin()
suppressReopenUntil = Date().addingTimeInterval(1.0)
+ didFinishLaunching = true
+ closeRestoredPreferencesWindowsDuringLaunch()
+ consumePendingShowOverlayInvocationIfNeeded()
configureApplicationMenuWhenAvailable(retries: 200)
warmAppIndexInBackground()
relocalizeDefaultAppNotesForCurrentLanguageAsync()
@@ -243,6 +255,54 @@
guard isRecentUserClickNearDockArea() else { return false }
showOrFocusOverlay()
return false // Suppress default "unhide all windows" behavior
+ }
+
+ func application(_ application: NSApplication, open urls: [URL]) {
+ for url in urls {
+ handleExternalInvocationURL(url)
+ }
+ }
+
+ private enum ExternalInvocationRoute {
+ case showOverlay
+ }
+
+ private func handleExternalInvocationURL(_ url: URL) {
+ guard externalInvocationRoute(for: url) == .showOverlay else {
+ Diagnostics.log("app.externalInvocation.ignored", [
+ "url": url.absoluteString
+ ])
+ return
+ }
+ requestShowOverlayFromExternalInvocation()
+ }
+
+ private func externalInvocationRoute(for url: URL) -> ExternalInvocationRoute? {
+ guard url.scheme?.lowercased() == Self.externalInvocationScheme else {
+ return nil
+ }
+ let host = url.host?.lowercased()
+ let path = url.path
+ if host == Self.externalInvocationShowHost, path.isEmpty || path == "/" {
+ return .showOverlay
+ }
+ return nil
+ }
+
+ private func requestShowOverlayFromExternalInvocation() {
+ guard didFinishLaunching else {
+ pendingShowOverlayInvocation = true
+ return
+ }
+ pendingShowOverlayInvocation = false
+ suppressReopenUntil = Date().addingTimeInterval(1.0)
+ dismissQuickSearchIfNeeded()
+ showOrFocusOverlay()
+ }
+
+ private func consumePendingShowOverlayInvocationIfNeeded() {
+ guard pendingShowOverlayInvocation else { return }
+ requestShowOverlayFromExternalInvocation()
}
private func isRecentUserClickNearDockArea() -> Bool {
@@ -293,6 +353,7 @@
hideOverlay(force: true, discardWindow: true)
unregisterHotkey(for: .main)
unregisterHotkey(for: .quickSearch)
+ unregisterOverlaySpaceHotkey()
TagDatabase.flushPendingCategorySchemeBackupBatch()
removeOverlayKeyMonitor()
removeQuickSearchExternalMouseMonitor()
@@ -480,14 +541,17 @@
if activate && requiresForegroundOwnership
&& !shouldStayAccessoryForCurrentFullscreenSpace
- && !shouldStayAccessoryForHiddenDockChrome
&& !shouldStayAccessoryForQuickOnlySearch {
let keyWindow = isSettingsVisible ? settingsWindow : (isOverlayVisible ? overlayWindow : nil)
- claimLauncherForeground(
- keyWindow: keyWindow,
- retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
- overlayGeneration: isOverlayVisible ? overlayGeneration : nil
- )
+ if shouldStayAccessoryForHiddenDockChrome {
+ beginLauncherForegroundOwnership(activate: true, keyWindow: keyWindow)
+ } else {
+ claimLauncherForeground(
+ keyWindow: keyWindow,
+ retries: isOverlayVisible && !isSettingsVisible ? 5 : 0,
+ overlayGeneration: isOverlayVisible ? overlayGeneration : nil
+ )
+ }
}
}
@@ -1298,9 +1362,27 @@
])
if id == LauncherHotkeyKind.quickSearch.eventID {
showQuickSearchFromGlobalHotkey()
+ } else if id == Self.overlaySpaceHotkeyID {
+ handleOverlaySpaceHotkey()
} else {
performToggleOverlay(preferredScreen: nil)
}
+ }
+
+ private func handleOverlaySpaceHotkey() {
+ guard canUseOverlaySpaceHotkey else { return }
+ unregisterOverlaySpaceHotkey()
+ requestQuickSearch(source: QuickSearchOpenSource.mainOverlay)
+ }
+
+ private var canUseOverlaySpaceHotkey: Bool {
+ overlayWindow?.isVisible == true
+ && !isSettingsVisible
+ && !isQuickSearchOpen
+ && !quickSearchOnlyOverlaySession
+ && !isInEditMode
+ && !isEditingAppNote
+ && !isModalInteractionActive
}
private func showQuickSearchFromGlobalHotkey() {
@@ -1358,6 +1440,46 @@
}
}
+ private func refreshOverlaySpaceHotkeyRegistration() {
+ if canUseOverlaySpaceHotkey {
+ registerOverlaySpaceHotkeyIfNeeded()
+ } else {
+ unregisterOverlaySpaceHotkey()
+ }
+ }
+
+ private func registerOverlaySpaceHotkeyIfNeeded() {
+ guard overlaySpaceHotkeyRef == nil else { return }
+
+ var hotkeyID = EventHotKeyID()
+ hotkeyID.signature = OSType(0x41505447) // 'APTG'
+ hotkeyID.id = Self.overlaySpaceHotkeyID
+
+ var newRef: EventHotKeyRef?
+ let status = RegisterEventHotKey(
+ UInt32(kVK_Space),
+ 0,
+ hotkeyID,
+ GetApplicationEventTarget(),
+ 0,
+ &newRef
+ )
+
+ if status == noErr, let newRef {
+ overlaySpaceHotkeyRef = newRef
+ } else {
+ overlaySpaceHotkeyRef = nil
+ Diagnostics.log("app.overlay.spaceHotkey.failed", ["status": status])
+ }
+ }
+
+ private func unregisterOverlaySpaceHotkey() {
+ if let overlaySpaceHotkeyRef {
+ UnregisterEventHotKey(overlaySpaceHotkeyRef)
+ self.overlaySpaceHotkeyRef = nil
+ }
+ }
+
// MARK: - Preferences
/// Hide overlay when any other window becomes key (catches Cmd+, via SwiftUI Settings).
@@ -1409,6 +1531,9 @@
forContentRect: NSRect(origin: .zero, size: settingsSize)
).size
window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
+ window.isRestorable = false
+ window.restorationClass = nil
+ _ = window.setFrameAutosaveName("")
window.contentMinSize = settingsSize
window.contentMaxSize = settingsSize
window.minSize = settingsFrameSize
@@ -1441,6 +1566,7 @@
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
settingsWindow = window
+ refreshOverlaySpaceHotkeyRegistration()
refreshLauncherChromeState(
activate: !overlayAvoidsSpaceSwitch,
avoidSpaceSwitch: overlayAvoidsSpaceSwitch
@@ -1545,6 +1671,7 @@
activate: shouldRefocusOverlay && !self.overlayAvoidsSpaceSwitch,
avoidSpaceSwitch: self.overlayAvoidsSpaceSwitch
)
+ self.refreshOverlaySpaceHotkeyRegistration()
guard shouldRefocusOverlay else { return }
DispatchQueue.main.asyncAfter(deadline: .now() + 0.12) { [weak self] in
self?.showOrFocusOverlay(preferredScreen: preferredScreen)
@@ -1559,7 +1686,9 @@
object: nil,
queue: .main
) { [weak self] notification in
- self?.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+ guard let self else { return }
+ self.isInEditMode = (notification.userInfo?["active"] as? Bool) ?? false
+ self.refreshOverlaySpaceHotkeyRegistration()
}
}
@@ -1576,6 +1705,7 @@
self.promoteOverlayToForegroundInput()
}
self.updateOverlayLevelForTextInput()
+ self.refreshOverlaySpaceHotkeyRegistration()
}
}
@@ -1642,6 +1772,7 @@
self.hideOverlay(force: true, discardWindow: wasQuickSearchOnlyOverlay)
}
}
+ self.refreshOverlaySpaceHotkeyRegistration()
}
NotificationCenter.default.addObserver(
@@ -1649,7 +1780,9 @@
object: nil,
queue: .main
) { [weak self] notification in
- self?.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+ guard let self else { return }
+ self.isModalInteractionActive = (notification.userInfo?["active"] as? Bool) ?? false
+ self.refreshOverlaySpaceHotkeyRegistration()
}
NotificationCenter.default.addObserver(
@@ -1757,6 +1890,7 @@
}
private func openPreferences(targetTab: String?) {
+ explicitPreferencesOpenRequestedAt = Date()
dismissQuickSearchIfNeeded()
TagDatabase.flushPendingCategorySchemeBackupBatch()
if overlayAvoidsSpaceSwitch {
@@ -1789,6 +1923,25 @@
prepareSettingsWindow(window)
}
+ private func closeRestoredPreferencesWindowsDuringLaunch() {
+ for delay in [0.25, 0.8, 1.6, 2.6] {
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+ self?.closeRestoredPreferencesWindowIfNeeded()
+ }
+ }
+ }
+
+ private func closeRestoredPreferencesWindowIfNeeded() {
+ guard Date().timeIntervalSince(explicitPreferencesOpenRequestedAt) > 3.0 else { return }
+ for window in NSApp.windows where isSettingsWindowCandidate(window) {
+ detachSettingsWindow(window)
+ window.close()
+ if settingsWindow == window {
+ settingsWindow = nil
+ }
+ }
+ }
+
private func requestPreferencesTabSelection(_ targetTab: String?) {
guard let targetTab else { return }
NotificationCenter.default.post(
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 479ca9a..f8b2ea1 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,20 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>8.1.4</string>
+ <string>8.1.6</string>
+ <key>CFBundleURLTypes</key>
+ <array>
+ <dict>
+ <key>CFBundleURLName</key>
+ <string>com.taglauncher.app</string>
+ <key>CFBundleURLSchemes</key>
+ <array>
+ <string>taglauncher</string>
+ </array>
+ </dict>
+ </array>
<key>CFBundleVersion</key>
- <string>20260627.1643</string>
+ <string>20260628.0230</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
diff --git a/src/Apptag/Localization/ar-Najdi.json b/src/Apptag/Localization/ar-Najdi.json
index d42ffa3..3f06a59 100644
--- a/src/Apptag/Localization/ar-Najdi.json
+++ b/src/Apptag/Localization/ar-Najdi.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "إيماءة لوحة التعقب",
+ "trackpadGesture.description": "اربط إيماءة لوحة التعقب في أداة خارجية بهالرابط عشان تعرض قائمة التطبيقات.",
+ "trackpadGesture.status": "أداة خارجية",
"smart.category.GTD": "المهام وGTD",
"smart.category.Notes": "الملاحظات",
"smart.category.Meeting": "الاجتماعات",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "معاينة موضوع برو",
"pro.theme.status.free": "المواضيع المجانية جاهزة الآن. فتح Pro للحفاظ على كل موضوع متقدم.",
"pro.theme.status.preview": "معاينة موضوع برو. قم بإلغاء تأمين Pro للاحتفاظ به بعد انتهاء المعاينة.",
+ "pro.theme.status.previewCountdown": "قيد التجربة · باقي %time%",
"pro.theme.status.unlocked": "تم فتح جميع السمات على جهاز Mac هذا."
}
diff --git a/src/Apptag/Localization/ar.json b/src/Apptag/Localization/ar.json
index a1012e0..224c707 100644
--- a/src/Apptag/Localization/ar.json
+++ b/src/Apptag/Localization/ar.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "إيماءة لوحة التعقب",
+ "trackpadGesture.description": "اربط إيماءة لوحة التعقب في أداة خارجية بهذا الرابط لعرض قائمة التطبيقات.",
+ "trackpadGesture.status": "أداة خارجية",
"smart.category.GTD": "المهام وGTD",
"smart.category.Notes": "الملاحظات",
"smart.category.Meeting": "الاجتماعات",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "معاينة موضوع برو",
"pro.theme.status.free": "المواضيع المجانية جاهزة الآن. فتح Pro للحفاظ على كل موضوع متقدم.",
"pro.theme.status.preview": "معاينة موضوع برو. قم بإلغاء تأمين Pro للاحتفاظ به بعد انتهاء المعاينة.",
+ "pro.theme.status.previewCountdown": "قيد المعاينة · متبقٍ %time%",
"pro.theme.status.unlocked": "تم فتح جميع السمات على جهاز Mac هذا."
}
diff --git a/src/Apptag/Localization/cs.json b/src/Apptag/Localization/cs.json
index 3e7868f..3790773 100644
--- a/src/Apptag/Localization/cs.json
+++ b/src/Apptag/Localization/cs.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gesto trackpadu",
+ "trackpadGesture.description": "Namapujte gesto trackpadu v nástroji třetí strany na tuto URL a zobrazte seznam aplikací.",
+ "trackpadGesture.status": "Externí nástroj",
"smart.category.GTD": "Úkoly a GTD",
"smart.category.Notes": "Poznámky",
"smart.category.Meeting": "Schůzky",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Náhled Pro téma",
"pro.theme.status.free": "Volná témata jsou nyní připravena. Odemkněte Pro, abyste zachovali každé pokročilé téma.",
"pro.theme.status.preview": "Zobrazení náhledu motivu Pro. Odemkněte Pro, abyste si jej ponechali i po skončení náhledu.",
+ "pro.theme.status.previewCountdown": "Náhled · zbývá %time%",
"pro.theme.status.unlocked": "Všechna témata jsou na tomto Macu odemčená."
}
diff --git a/src/Apptag/Localization/da.json b/src/Apptag/Localization/da.json
index 8e4a9e8..3ef0339 100644
--- a/src/Apptag/Localization/da.json
+++ b/src/Apptag/Localization/da.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Pegefeltbevægelse",
+ "trackpadGesture.description": "Knyt en pegefeltbevægelse i et tredjepartsværktøj til denne URL for at vise applisten.",
+ "trackpadGesture.status": "Eksternt værktøj",
"smart.category.GTD": "Opgaver og GTD",
"smart.category.Notes": "Noter",
"smart.category.Meeting": "Møder",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Forhåndsvisning af Pro-tema",
"pro.theme.status.free": "Gratis temaer er klar nu. Lås Pro op for at beholde alle avancerede temaer.",
"pro.theme.status.preview": "Forhåndsvisning af et Pro-tema. Lås Pro op for at beholde den, når forhåndsvisningen slutter.",
+ "pro.theme.status.previewCountdown": "Forhåndsvisning · %time% tilbage",
"pro.theme.status.unlocked": "Alle temaer er låst op på denne Mac."
}
diff --git a/src/Apptag/Localization/de.json b/src/Apptag/Localization/de.json
index f087d60..b108400 100644
--- a/src/Apptag/Localization/de.json
+++ b/src/Apptag/Localization/de.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Trackpad-Geste",
+ "trackpadGesture.description": "Ordnen Sie in einem Drittanbieter-Tool eine Trackpad-Geste dieser URL zu, um die App-Liste anzuzeigen.",
+ "trackpadGesture.status": "Externes Tool",
"smart.category.GTD": "Aufgaben und GTD",
"smart.category.Notes": "Notizen",
"smart.category.Meeting": "Besprechungen",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Vorschau des Pro-Themes",
"pro.theme.status.free": "Kostenlose Themes sind jetzt fertig. Schalten Sie Pro frei, um alle erweiterten Themes beizubehalten.",
"pro.theme.status.preview": "Vorschau eines Pro-Themes. Schalten Sie Pro frei, um es nach Ende der Vorschau zu behalten.",
+ "pro.theme.status.previewCountdown": "Vorschau · noch %time%",
"pro.theme.status.unlocked": "Alle Themes sind auf diesem Mac freigeschaltet."
}
diff --git a/src/Apptag/Localization/en.json b/src/Apptag/Localization/en.json
index 2921d09..72c4c0d 100644
--- a/src/Apptag/Localization/en.json
+++ b/src/Apptag/Localization/en.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Trackpad gesture",
+ "trackpadGesture.description": "Map a trackpad gesture in a third-party tool to this URL to show the app list.",
+ "trackpadGesture.status": "External tool",
"smart.category.GTD": "Tasks & GTD",
"smart.category.Notes": "Notes",
"smart.category.Meeting": "Meetings",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Previewing Pro theme",
"pro.theme.status.free": "Free themes are ready now. Unlock Pro to keep every advanced theme.",
"pro.theme.status.preview": "Previewing a Pro theme. Unlock Pro to keep it after the preview ends.",
+ "pro.theme.status.previewCountdown": "Previewing · %time% left",
"pro.theme.status.unlocked": "All themes are unlocked on this Mac."
}
diff --git a/src/Apptag/Localization/es.json b/src/Apptag/Localization/es.json
index 45d8aad..df496e0 100644
--- a/src/Apptag/Localization/es.json
+++ b/src/Apptag/Localization/es.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gesto del trackpad",
+ "trackpadGesture.description": "Asigna un gesto del trackpad en una herramienta de terceros a esta URL para mostrar la lista de apps.",
+ "trackpadGesture.status": "Herramienta externa",
"smart.category.GTD": "Tareas y GTD",
"smart.category.Notes": "Notas",
"smart.category.Meeting": "Reuniones",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Vista previa del tema Pro",
"pro.theme.status.free": "Los temas gratuitos ya están listos. Desbloquea Pro para conservar todos los temas avanzados.",
"pro.theme.status.preview": "Vista previa de un tema Pro. Desbloquea Pro para conservarlo después de que finalice la vista previa.",
+ "pro.theme.status.previewCountdown": "Vista previa · quedan %time%",
"pro.theme.status.unlocked": "Todos los temas están desbloqueados en esta Mac."
}
diff --git a/src/Apptag/Localization/fr.json b/src/Apptag/Localization/fr.json
index 599fb7c..e9adb97 100644
--- a/src/Apptag/Localization/fr.json
+++ b/src/Apptag/Localization/fr.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Geste du trackpad",
+ "trackpadGesture.description": "Associez un geste du trackpad dans un outil tiers à cette URL pour afficher la liste des apps.",
+ "trackpadGesture.status": "Outil tiers",
"smart.category.GTD": "Tâches et GTD",
"smart.category.Notes": "Prises de notes",
"smart.category.Meeting": "Réunions",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Aperçu du thème Pro",
"pro.theme.status.free": "Les thèmes gratuits sont prêts maintenant. Débloquez Pro pour conserver tous les thèmes avancés.",
"pro.theme.status.preview": "Prévisualisation d'un thème Pro. Déverrouillez Pro pour le conserver une fois l'aperçu terminé.",
+ "pro.theme.status.previewCountdown": "Aperçu · %time% restantes",
"pro.theme.status.unlocked": "Tous les thèmes sont débloqués sur ce Mac."
}
diff --git a/src/Apptag/Localization/id.json b/src/Apptag/Localization/id.json
index 2add81c..7944e87 100644
--- a/src/Apptag/Localization/id.json
+++ b/src/Apptag/Localization/id.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gestur trackpad",
+ "trackpadGesture.description": "Petakan gestur trackpad di alat pihak ketiga ke URL ini untuk menampilkan daftar app.",
+ "trackpadGesture.status": "Alat eksternal",
"smart.category.GTD": "Tugas dan GTD",
"smart.category.Notes": "Catatan",
"smart.category.Meeting": "Rapat",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Pratinjau tema Pro",
"pro.theme.status.free": "Tema gratis sudah siap sekarang. Buka kunci Pro untuk mempertahankan setiap tema lanjutan.",
"pro.theme.status.preview": "Pratinjau tema Pro. Buka kunci Pro untuk menyimpannya setelah pratinjau berakhir.",
+ "pro.theme.status.previewCountdown": "Pratinjau · tersisa %time%",
"pro.theme.status.unlocked": "Semua tema tidak terkunci di Mac ini."
}
diff --git a/src/Apptag/Localization/it.json b/src/Apptag/Localization/it.json
index 8c39924..5bca6ba 100644
--- a/src/Apptag/Localization/it.json
+++ b/src/Apptag/Localization/it.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gesto del trackpad",
+ "trackpadGesture.description": "Associa un gesto del trackpad in uno strumento di terze parti a questo URL per mostrare l’elenco delle app.",
+ "trackpadGesture.status": "Strumento esterno",
"smart.category.GTD": "Attività e GTD",
"smart.category.Notes": "Note",
"smart.category.Meeting": "Riunioni",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Anteprima del tema Pro",
"pro.theme.status.free": "I temi gratuiti sono pronti ora. Sblocca Pro per mantenere tutti i temi avanzati.",
"pro.theme.status.preview": "Anteprima di un tema Pro. Sblocca Pro per conservarlo al termine dell'anteprima.",
+ "pro.theme.status.previewCountdown": "Anteprima · %time% rimasti",
"pro.theme.status.unlocked": "Tutti i temi sono sbloccati su questo Mac."
}
diff --git a/src/Apptag/Localization/ja.json b/src/Apptag/Localization/ja.json
index c5248ed..3538026 100644
--- a/src/Apptag/Localization/ja.json
+++ b/src/Apptag/Localization/ja.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "トラックパッドジェスチャ",
+ "trackpadGesture.description": "サードパーティ製ツールでトラックパッドジェスチャをこのURLに割り当てると、アプリ一覧を表示できます。",
+ "trackpadGesture.status": "外部ツール",
"smart.category.GTD": "タスクとGTD",
"smart.category.Notes": "メモ",
"smart.category.Meeting": "会議",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Pro テーマのプレビュー",
"pro.theme.status.free": "無料のテーマが準備完了です。 Pro のロックを解除して、すべての高度なテーマを保持します。",
"pro.theme.status.preview": "Pro テーマをプレビューしています。プレビュー終了後も保持するには、Pro のロックを解除してください。",
+ "pro.theme.status.previewCountdown": "プレビュー中 · 残り %time%",
"pro.theme.status.unlocked": "この Mac ではすべてのテーマのロックが解除されています。"
}
diff --git a/src/Apptag/Localization/ko.json b/src/Apptag/Localization/ko.json
index 662ce02..64ed521 100644
--- a/src/Apptag/Localization/ko.json
+++ b/src/Apptag/Localization/ko.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "트랙패드 제스처",
+ "trackpadGesture.description": "서드파티 도구에서 트랙패드 제스처를 이 URL에 연결하면 앱 목록이 열립니다.",
+ "trackpadGesture.status": "외부 도구",
"smart.category.GTD": "작업 및 GTD",
"smart.category.Notes": "메모",
"smart.category.Meeting": "회의",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Pro 테마 미리보기",
"pro.theme.status.free": "무료 테마가 이제 준비되었습니다. 모든 고급 테마를 유지하려면 Pro을(를) 잠금 해제하세요.",
"pro.theme.status.preview": "Pro 테마를 미리 보는 중입니다. 미리보기가 끝난 후에도 유지하려면 Pro을(를) 잠금 해제하세요.",
+ "pro.theme.status.previewCountdown": "미리보기 중 · %time% 남음",
"pro.theme.status.unlocked": "모든 테마는 이 Mac에서 잠금 해제됩니다."
}
diff --git a/src/Apptag/Localization/ms.json b/src/Apptag/Localization/ms.json
index 357d4a6..d36d461 100644
--- a/src/Apptag/Localization/ms.json
+++ b/src/Apptag/Localization/ms.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gerak isyarat pad jejak",
+ "trackpadGesture.description": "Petakan gerak isyarat pad jejak dalam alat pihak ketiga kepada URL ini untuk memaparkan senarai app.",
+ "trackpadGesture.status": "Alat luaran",
"smart.category.GTD": "Tugasan dan GTD",
"smart.category.Notes": "Nota",
"smart.category.Meeting": "Mesyuarat",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Mempratonton tema Pro",
"pro.theme.status.free": "Tema percuma sudah sedia sekarang. Buka kunci Pro untuk mengekalkan setiap tema lanjutan.",
"pro.theme.status.preview": "Mempratonton tema Pro. Buka kunci Pro untuk menyimpannya selepas pratonton tamat.",
+ "pro.theme.status.previewCountdown": "Pratonton · %time% lagi",
"pro.theme.status.unlocked": "Semua tema dibuka kunci pada Mac ini."
}
diff --git a/src/Apptag/Localization/nb.json b/src/Apptag/Localization/nb.json
index 16b71ab..6a09b1c 100644
--- a/src/Apptag/Localization/nb.json
+++ b/src/Apptag/Localization/nb.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Styreflatebevegelse",
+ "trackpadGesture.description": "Koble en styreflatebevegelse i et tredjepartsverktøy til denne URL-en for å vise applisten.",
+ "trackpadGesture.status": "Eksternt verktøy",
"smart.category.GTD": "Oppgaver og GTD",
"smart.category.Notes": "Notater",
"smart.category.Meeting": "Møter",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Forhåndsviser Pro-tema",
"pro.theme.status.free": "Gratis temaer er klare nå. Lås opp Pro for å beholde alle avanserte temaer.",
"pro.theme.status.preview": "Forhåndsvisning av et Pro-tema. Lås opp Pro for å beholde den etter at forhåndsvisningen er ferdig.",
+ "pro.theme.status.previewCountdown": "Forhåndsvisning · %time% igjen",
"pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac."
}
diff --git a/src/Apptag/Localization/nl.json b/src/Apptag/Localization/nl.json
index a2ee3ce..c438d8e 100644
--- a/src/Apptag/Localization/nl.json
+++ b/src/Apptag/Localization/nl.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Trackpadgebaar",
+ "trackpadGesture.description": "Koppel in een tool van derden een trackpadgebaar aan deze URL om de applijst te tonen.",
+ "trackpadGesture.status": "Externe tool",
"smart.category.GTD": "Taken en GTD",
"smart.category.Notes": "Notities",
"smart.category.Meeting": "Vergaderingen",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Een voorbeeld bekijken van het Pro-thema",
"pro.theme.status.free": "Gratis thema's zijn nu klaar. Ontgrendel Pro om elk geavanceerd thema te behouden.",
"pro.theme.status.preview": "Een voorbeeld bekijken van een Pro thema. Ontgrendel Pro om deze te behouden nadat de preview is afgelopen.",
+ "pro.theme.status.previewCountdown": "Voorvertoning · nog %time%",
"pro.theme.status.unlocked": "Alle thema's zijn ontgrendeld op deze Mac."
}
diff --git a/src/Apptag/Localization/nn.json b/src/Apptag/Localization/nn.json
index 51f9c04..4a6f3d4 100644
--- a/src/Apptag/Localization/nn.json
+++ b/src/Apptag/Localization/nn.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Styreflategest",
+ "trackpadGesture.description": "Kopla ein styreflategest i eit tredjepartsverktøy til denne URL-en for å visa applista.",
+ "trackpadGesture.status": "Eksternt verktøy",
"smart.category.GTD": "Oppgåver og GTD",
"smart.category.Notes": "Notat",
"smart.category.Meeting": "Møte",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Forhåndsviser Pro-tema",
"pro.theme.status.free": "Gratis temaer er klare nå. Lås opp Pro for å beholde alle avanserte temaer.",
"pro.theme.status.preview": "Forhåndsvisning av et Pro-tema. Lås opp Pro for å beholde den etter at forhåndsvisningen er ferdig.",
+ "pro.theme.status.previewCountdown": "Førehandsvising · %time% att",
"pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac."
}
diff --git a/src/Apptag/Localization/no.json b/src/Apptag/Localization/no.json
index 16b71ab..6a09b1c 100644
--- a/src/Apptag/Localization/no.json
+++ b/src/Apptag/Localization/no.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Styreflatebevegelse",
+ "trackpadGesture.description": "Koble en styreflatebevegelse i et tredjepartsverktøy til denne URL-en for å vise applisten.",
+ "trackpadGesture.status": "Eksternt verktøy",
"smart.category.GTD": "Oppgaver og GTD",
"smart.category.Notes": "Notater",
"smart.category.Meeting": "Møter",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Forhåndsviser Pro-tema",
"pro.theme.status.free": "Gratis temaer er klare nå. Lås opp Pro for å beholde alle avanserte temaer.",
"pro.theme.status.preview": "Forhåndsvisning av et Pro-tema. Lås opp Pro for å beholde den etter at forhåndsvisningen er ferdig.",
+ "pro.theme.status.previewCountdown": "Forhåndsvisning · %time% igjen",
"pro.theme.status.unlocked": "Alle temaer er låst opp på denne Mac."
}
diff --git a/src/Apptag/Localization/pl.json b/src/Apptag/Localization/pl.json
index b61b7f3..b6ef136 100644
--- a/src/Apptag/Localization/pl.json
+++ b/src/Apptag/Localization/pl.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gest gładzika",
+ "trackpadGesture.description": "Przypisz gest gładzika w narzędziu innej firmy do tego URL-a, aby pokazać listę aplikacji.",
+ "trackpadGesture.status": "Narzędzie zewnętrzne",
"smart.category.GTD": "Zadania i GTD",
"smart.category.Notes": "Notatki",
"smart.category.Meeting": "Spotkania",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Podgląd motywu Pro",
"pro.theme.status.free": "Darmowe motywy są już gotowe. Odblokuj Pro, aby zachować każdy zaawansowany motyw.",
"pro.theme.status.preview": "Podgląd motywu Pro. Odblokuj Pro, aby zachować go po zakończeniu podglądu.",
+ "pro.theme.status.previewCountdown": "Podgląd · zostało %time%",
"pro.theme.status.unlocked": "Wszystkie motywy są odblokowane na tym Mac."
}
diff --git a/src/Apptag/Localization/pt-BR.json b/src/Apptag/Localization/pt-BR.json
index 3a17fd9..b6f35a4 100644
--- a/src/Apptag/Localization/pt-BR.json
+++ b/src/Apptag/Localization/pt-BR.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gesto do trackpad",
+ "trackpadGesture.description": "Mapeie um gesto do trackpad em uma ferramenta de terceiros para esta URL para mostrar a lista de apps.",
+ "trackpadGesture.status": "Ferramenta externa",
"smart.category.GTD": "Tarefas e GTD",
"smart.category.Notes": "Notas",
"smart.category.Meeting": "Reuniões",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Visualizando o tema Pro",
"pro.theme.status.free": "Os temas gratuitos estão prontos agora. Desbloqueie Pro para manter todos os temas avançados.",
"pro.theme.status.preview": "Visualizando um tema Pro. Desbloqueie Pro para mantê-lo após o término da visualização.",
+ "pro.theme.status.previewCountdown": "Prévia · faltam %time%",
"pro.theme.status.unlocked": "Todos os temas são desbloqueados neste Mac."
}
diff --git a/src/Apptag/Localization/ro.json b/src/Apptag/Localization/ro.json
index f5f70e7..a0fedd2 100644
--- a/src/Apptag/Localization/ro.json
+++ b/src/Apptag/Localization/ro.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Gest trackpad",
+ "trackpadGesture.description": "Asociază un gest de trackpad dintr-un instrument terț cu acest URL pentru a afișa lista de aplicații.",
+ "trackpadGesture.status": "Instrument extern",
"smart.category.GTD": "Sarcini și GTD",
"smart.category.Notes": "Note",
"smart.category.Meeting": "Întâlniri",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Previzualizarea temei Pro",
"pro.theme.status.free": "Temele gratuite sunt gata acum. Deblocați Pro pentru a păstra fiecare temă avansată.",
"pro.theme.status.preview": "Previzualizarea unei teme Pro. Deblocați Pro pentru a-l păstra după terminarea previzualizării.",
+ "pro.theme.status.previewCountdown": "Previzualizare · %time% rămase",
"pro.theme.status.unlocked": "Toate temele sunt deblocate pe acest Mac."
}
diff --git a/src/Apptag/Localization/ru.json b/src/Apptag/Localization/ru.json
index 0238ffe..3238dd8 100644
--- a/src/Apptag/Localization/ru.json
+++ b/src/Apptag/Localization/ru.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Жест трекпада",
+ "trackpadGesture.description": "Назначьте жест трекпада в сторонней утилите на этот URL, чтобы открыть список приложений.",
+ "trackpadGesture.status": "Внешний инструмент",
"smart.category.GTD": "Задачи и GTD",
"smart.category.Notes": "Заметки",
"smart.category.Meeting": "Встречи",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Предварительный просмотр темы Pro",
"pro.theme.status.free": "Бесплатные темы уже готовы. Разблокируйте Pro, чтобы сохранить все расширенные темы.",
"pro.theme.status.preview": "Предварительный просмотр темы Pro. Разблокируйте Pro, чтобы сохранить его после завершения предварительного просмотра.",
+ "pro.theme.status.previewCountdown": "Предпросмотр · осталось %time%",
"pro.theme.status.unlocked": "На этом Mac разблокированы все темы."
}
diff --git a/src/Apptag/Localization/sr-Cyrl.json b/src/Apptag/Localization/sr-Cyrl.json
index af1ca34..13ae31b 100644
--- a/src/Apptag/Localization/sr-Cyrl.json
+++ b/src/Apptag/Localization/sr-Cyrl.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Гест на трекпеду",
+ "trackpadGesture.description": "Повежите гест на трекпеду у алату треће стране са овом URL адресом да бисте приказали листу апликација.",
+ "trackpadGesture.status": "Спољни алат",
"smart.category.GTD": "Задаци и GTD",
"smart.category.Notes": "Белешке",
"smart.category.Meeting": "Састанци",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Преглед Pro теме",
"pro.theme.status.free": "Бесплатне теме су сада спремне. Откључајте Pro да бисте задржали сваку напредну тему.",
"pro.theme.status.preview": "Прегледате Pro тему. Откључајте Pro да бисте је задржали након завршетка прегледа.",
+ "pro.theme.status.previewCountdown": "Преглед · преостало %time%",
"pro.theme.status.unlocked": "Све теме су откључане на овом Mac-у."
}
diff --git a/src/Apptag/Localization/sv.json b/src/Apptag/Localization/sv.json
index 48e3b51..66c6569 100644
--- a/src/Apptag/Localization/sv.json
+++ b/src/Apptag/Localization/sv.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Styrplattegest",
+ "trackpadGesture.description": "Koppla en styrplattegest i ett tredjepartsverktyg till denna URL för att visa applistan.",
+ "trackpadGesture.status": "Externt verktyg",
"smart.category.GTD": "Uppgifter och GTD",
"smart.category.Notes": "Anteckningar",
"smart.category.Meeting": "Möten",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Förhandsgranskning av Pro-tema",
"pro.theme.status.free": "Gratis teman är klara nu. Lås upp Pro för att behålla alla avancerade teman.",
"pro.theme.status.preview": "Förhandsgranskning av ett Pro-tema. Lås upp Pro för att behålla den efter att förhandsvisningen är slut.",
+ "pro.theme.status.previewCountdown": "Förhandsvisning · %time% kvar",
"pro.theme.status.unlocked": "Alla teman är upplåsta på denna Mac."
}
diff --git a/src/Apptag/Localization/th.json b/src/Apptag/Localization/th.json
index bdb104f..ab3adea 100644
--- a/src/Apptag/Localization/th.json
+++ b/src/Apptag/Localization/th.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "ท่าทางแทร็คแพด",
+ "trackpadGesture.description": "จับคู่ท่าทางแทร็คแพดในเครื่องมือภายนอกกับ URL นี้เพื่อแสดงรายการแอป",
+ "trackpadGesture.status": "เครื่องมือภายนอก",
"smart.category.GTD": "งานและ GTD",
"smart.category.Notes": "โน้ต",
"smart.category.Meeting": "การประชุม",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "กำลังดูตัวอย่างธีม Pro",
"pro.theme.status.free": "ธีมฟรีพร้อมใช้งานแล้ว ปลดล็อก Pro เพื่อเก็บธีมขั้นสูงไว้ทุกอัน",
"pro.theme.status.preview": "การดูตัวอย่างธีม Pro ปลดล็อก Pro เพื่อเก็บไว้หลังจากการดูตัวอย่างสิ้นสุดลง",
+ "pro.theme.status.previewCountdown": "กำลังดูตัวอย่าง · เหลือ %time%",
"pro.theme.status.unlocked": "ปลดล็อคธีมทั้งหมดแล้วใน Mac นี้"
}
diff --git a/src/Apptag/Localization/tr.json b/src/Apptag/Localization/tr.json
index aa1d40f..ff2491e 100644
--- a/src/Apptag/Localization/tr.json
+++ b/src/Apptag/Localization/tr.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "İzleme dörtgeni hareketi",
+ "trackpadGesture.description": "Uygulama listesini göstermek için üçüncü taraf bir araçta izleme dörtgeni hareketini bu URL’ye bağlayın.",
+ "trackpadGesture.status": "Harici araç",
"smart.category.GTD": "Görevler ve GTD",
"smart.category.Notes": "Notlar",
"smart.category.Meeting": "Toplantılar",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Pro temasının önizlemesi yapılıyor",
"pro.theme.status.free": "Ücretsiz temalar artık hazır. Tüm gelişmiş temaları korumak için Pro'nun kilidini açın.",
"pro.theme.status.preview": "Bir Pro temasının önizlenmesi. Önizleme sona erdikten sonra onu korumak için Pro kilidini açın.",
+ "pro.theme.status.previewCountdown": "Önizleme · %time% kaldı",
"pro.theme.status.unlocked": "Bu Mac üzerinde tüm temaların kilidi açıldı."
}
diff --git a/src/Apptag/Localization/uk.json b/src/Apptag/Localization/uk.json
index 7fd913b..ebba9f2 100644
--- a/src/Apptag/Localization/uk.json
+++ b/src/Apptag/Localization/uk.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Жест трекпада",
+ "trackpadGesture.description": "Призначте жест трекпада у сторонній утиліті на цю URL-адресу, щоб показати список програм.",
+ "trackpadGesture.status": "Зовнішній інструмент",
"smart.category.GTD": "Завдання і GTD",
"smart.category.Notes": "Нотатки",
"smart.category.Meeting": "Зустрічі",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "попередній перегляд теми Pro",
"pro.theme.status.free": "безкоштовні теми готові. Розблокуйте Pro, щоб зберегти всі розширені теми.",
"pro.theme.status.preview": "попередній перегляд теми Pro. Розблокуйте Pro, щоб зберегти його після закінчення попереднього перегляду.",
+ "pro.theme.status.previewCountdown": "Попередній перегляд · залишилось %time%",
"pro.theme.status.unlocked": "усі теми розблоковано на цьому Mac."
}
diff --git a/src/Apptag/Localization/vi.json b/src/Apptag/Localization/vi.json
index c9a3da7..638562d 100644
--- a/src/Apptag/Localization/vi.json
+++ b/src/Apptag/Localization/vi.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "Quick Search (global)",
"quickSearch.globalHotkeyDesc": "Open Quick Search globally when the TagLauncher app list is not visible.",
"quickSearch.spaceDisplay": "Space",
+ "trackpadGesture.title": "Cử chỉ bàn di chuột",
+ "trackpadGesture.description": "Gán một cử chỉ bàn di chuột trong công cụ bên thứ ba vào URL này để hiện danh sách ứng dụng.",
+ "trackpadGesture.status": "Công cụ bên ngoài",
"smart.category.GTD": "Công việc và GTD",
"smart.category.Notes": "Ghi chú",
"smart.category.Meeting": "Cuộc họp",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "Xem trước chủ đề Pro",
"pro.theme.status.free": "Các chủ đề miễn phí hiện đã sẵn sàng. Mở khóa Pro để giữ mọi chủ đề nâng cao.",
"pro.theme.status.preview": "Xem trước chủ đề Pro. Mở khóa Pro để giữ nó sau khi bản xem trước kết thúc.",
+ "pro.theme.status.previewCountdown": "Đang xem thử · còn %time%",
"pro.theme.status.unlocked": "Tất cả chủ đề đều được mở khóa trên Mac này."
}
diff --git a/src/Apptag/Localization/zh-Hans.json b/src/Apptag/Localization/zh-Hans.json
index fd1ca3b..3ca718d 100644
--- a/src/Apptag/Localization/zh-Hans.json
+++ b/src/Apptag/Localization/zh-Hans.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "快捷搜索(全局)",
"quickSearch.globalHotkeyDesc": "不在 TagLauncher 主界面时,直接打开快捷搜索。全局有效。",
"quickSearch.spaceDisplay": "Space(空格键)",
+ "trackpadGesture.title": "触控板手势",
+ "trackpadGesture.description": "在第三方手势工具中把触控板手势映射到这个 URL,即可显示应用列表。",
+ "trackpadGesture.status": "外部工具",
"smart.category.GTD": "任务与 GTD",
"smart.category.Notes": "笔记",
"smart.category.Meeting": "会议",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "正在预览 Pro 主题",
"pro.theme.status.free": "免费主题已经可用。解锁 Pro 后可长期使用全部高级主题。",
"pro.theme.status.preview": "正在预览 Pro 主题。解锁 Pro 后可在预览结束后继续保留。",
+ "pro.theme.status.previewCountdown": "体验中 · 剩余 %time%",
"pro.theme.status.unlocked": "这台 Mac 上的全部主题都已解锁。"
}
diff --git a/src/Apptag/Localization/zh-Hant.json b/src/Apptag/Localization/zh-Hant.json
index 7eff870..a2d562b 100644
--- a/src/Apptag/Localization/zh-Hant.json
+++ b/src/Apptag/Localization/zh-Hant.json
@@ -187,6 +187,9 @@
"quickSearch.globalHotkey": "快捷搜尋(全域)",
"quickSearch.globalHotkeyDesc": "不在 TagLauncher 主介面時,直接打開快捷搜尋。全域有效。",
"quickSearch.spaceDisplay": "Space(空格鍵)",
+ "trackpadGesture.title": "觸控板手勢",
+ "trackpadGesture.description": "在第三方手勢工具中把觸控板手勢映射到這個 URL,即可顯示應用列表。",
+ "trackpadGesture.status": "外部工具",
"smart.category.GTD": "任務與 GTD",
"smart.category.Notes": "筆記",
"smart.category.Meeting": "會議",
@@ -290,5 +293,6 @@
"pro.theme.preview.status": "正在預覽 Pro 主題",
"pro.theme.status.free": "免費主題已可使用。解鎖 Pro 後可長期使用全部進階主題。",
"pro.theme.status.preview": "正在預覽 Pro 主題。解鎖 Pro 後可在預覽結束後繼續保留。",
+ "pro.theme.status.previewCountdown": "體驗中 · 剩餘 %time%",
"pro.theme.status.unlocked": "這台 Mac 上的全部主題都已解鎖。"
}
diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 52b09ab..97c2152 100644
--- a/src/Apptag/PreferencesView.swift
+++ b/src/Apptag/PreferencesView.swift
@@ -3,6 +3,8 @@
// MARK: - Preferences View
+private let themePreviewCountdownTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
+
private enum SettingsTab: String, CaseIterable, Identifiable {
case language
case general
@@ -85,6 +87,7 @@
@State private var selectedTab: SettingsTab = .general
@State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
@State private var settingsProPromptFeature: ProFeature? = nil
+ @State private var themePreviewNow = Date()
init(initialTabRawValue: String? = nil) {
let initialTab = initialTabRawValue.flatMap(SettingsTab.init(rawValue:)) ?? .general
@@ -610,6 +613,10 @@
private var themeStatusSummary: String {
if proEntitlement.isPreviewingTheme {
+ if let countdown = themePreviewCountdownText {
+ return tr("pro.theme.status.previewCountdown")
+ .replacingOccurrences(of: "%time%", with: countdown)
+ }
return tr("pro.theme.status.preview")
}
switch proEntitlement.accessState {
@@ -635,7 +642,7 @@
private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory {
if let preview = proEntitlement.themePreviewState?.theme, preview == theme {
- return .pill(text: tr("pro.card.previewBadge"), style: .preview)
+ return .countdown(text: themePreviewCountdownText ?? "00:00")
}
if !proEntitlement.isUnlocked && theme.requiresPro {
return .pill(text: tr("pro.card.badge"), style: .locked)
@@ -647,6 +654,24 @@
proEntitlement.clearTransientOperationState()
withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
settingsProPromptFeature = feature
+ }
+ }
+
+ private var themePreviewCountdownText: String? {
+ guard let preview = proEntitlement.themePreviewState else { return nil }
+ return Self.formatThemePreviewCountdown(seconds: preview.remainingSeconds(at: themePreviewNow))
+ }
+
+ private static func formatThemePreviewCountdown(seconds: Int) -> String {
+ let clamped = max(0, seconds)
+ return String(format: "%02d:%02d", clamped / 60, clamped % 60)
+ }
+
+ private func refreshThemePreviewCountdown(now: Date = Date()) {
+ themePreviewNow = now
+ guard let preview = proEntitlement.themePreviewState else { return }
+ if preview.remainingSeconds(at: now) <= 0 {
+ proEntitlement.stopThemePreview()
}
}
@@ -761,6 +786,14 @@
displayText: LauncherHotkey.quickSearch.displayString,
statusText: hotkeyStatusText(for: .quickSearch),
statusTone: hotkeyStatusTone(for: .quickSearch)
+ )
+
+ StaticHotkeyInfoRow(
+ title: tr("trackpadGesture.title"),
+ description: tr("trackpadGesture.description"),
+ displayText: "taglauncher://show",
+ statusText: tr("trackpadGesture.status"),
+ statusTone: .neutral
)
}
}
@@ -1473,6 +1506,10 @@
.onAppear {
syncSelectedLanguage()
showPendingHotkeyWarningIfNeeded()
+ refreshThemePreviewCountdown()
+ }
+ .onReceive(themePreviewCountdownTimer) { now in
+ refreshThemePreviewCountdown(now: now)
}
.onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in
showPendingHotkeyWarningIfNeeded()
@@ -1662,6 +1699,7 @@
private enum ThemeOptionAccessory {
case checkmark
case circle
+ case countdown(text: String)
case pill(text: String, style: ProStatusPillStyle)
}
@@ -1739,6 +1777,22 @@
Image(systemName: "circle")
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(Color.secondary.opacity(0.42))
+ case .countdown(let text):
+ Text(text)
+ .font(.system(size: 10, weight: .semibold, design: .monospaced))
+ .foregroundStyle(Color(red: 0.30, green: 0.22, blue: 0.08))
+ .lineLimit(1)
+ .frame(minWidth: 42)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 4)
+ .background(
+ Capsule(style: .continuous)
+ .fill(Color.orange.opacity(0.14))
+ )
+ .overlay(
+ Capsule(style: .continuous)
+ .stroke(Color.orange.opacity(0.28), lineWidth: 1)
+ )
case .pill(let text, let style):
ProStatusPill(text: text, style: style, compact: true)
}
diff --git a/src/Apptag/ProEntitlement.swift b/src/Apptag/ProEntitlement.swift
index e1c8c5e..4841e55 100644
--- a/src/Apptag/ProEntitlement.swift
+++ b/src/Apptag/ProEntitlement.swift
@@ -97,6 +97,10 @@
let theme: AppGridTheme
let startedAt: Date
let endsAt: Date
+
+ func remainingSeconds(at date: Date = Date()) -> Int {
+ max(0, Int(ceil(endsAt.timeIntervalSince(date))))
+ }
}
struct ProNoteQuotaStatus: Equatable {
diff --git a/src/Apptag/SmartCategorization/SmartCategory.swift b/src/Apptag/SmartCategorization/SmartCategory.swift
index 4fc10f0..949bdd3 100644
--- a/src/Apptag/SmartCategorization/SmartCategory.swift
+++ b/src/Apptag/SmartCategorization/SmartCategory.swift
@@ -251,6 +251,8 @@
var smartStartDefaultCategoryID: SmartCategoryID? {
switch self {
+ case .meeting:
+ return .communication
case .pdf, .productivity, .writing:
return .office
case .fileManagement, .transfer, .utilities, .system, .systemMaintenance,
@@ -295,7 +297,6 @@
.communication,
.gtd,
.notes,
- .meeting,
.office,
.systemEnhancement,
.aiTools,
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 00d040c..0ecfb57 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -2,6 +2,27 @@
## [Unreleased]
+## [8.1.6] — 2026-06-28
+
+- Pro 主题免费体验增加可见倒计时:主题页状态行显示剩余时间,当前预览主题卡片显示 `MM:SS`,体验到期后静默恢复到用户真实可用主题
+- 倒计时从真实 `ProThemePreviewState.endsAt` 推导,不在文案中写死 5 分钟;切换 Pro 主题会重置计时,选择免费主题会停止预览并保存免费主题
+- SmartStart 智能初始化默认标签从 13 个进一步收敛为 12 个:`Meeting / 会议` 不再单独创建默认标签,统一归入 `Communication / 沟通`
+- 补齐 29 个语种的 Pro 主题倒计时文案,并更新 SmartStart 默认标签需求文档与 QA 门禁
+- 修复隐藏 Dock 图标时 AppGrid 通过热键打开后键盘焦点不稳定的问题,确保 `Esc` 可正常关闭面板且不会重新显示 Dock 图标
+- 修复全屏 Space 中 AppGrid 按 `Space` 打开 Quick Search 的键盘路由:仅在 AppGrid 可见且非编辑 / 非设置 / 非文本输入状态下临时接管裸 `Space`,不新增系统权限
+- 加固窗口回归门禁,覆盖 Dock 隐藏、Quick Search、设置窗口、文件面板和全屏 Space 的层级与键盘路径
+- 版本号更新为 `8.1.6`,Build 更新为 `20260628.0230`
+
+## [8.1.5] — 2026-06-27
+
+- 新增外部 URL 唤起入口:`taglauncher://show`,可由 BetterTouchTool 等第三方手势工具映射触控板手势后稳定显示 / 聚焦 App Grid
+- `taglauncher://show` 复用现有 AppGrid 显示链路,不做 toggle,不接管系统原生触控板手势,不新增 Accessibility / Input Monitoring 权限,也不使用私有 multitouch API
+- 冷启动收到 URL 唤起时会排队到 App 初始化完成后再显示 App Grid;运行中收到 URL 唤起时会清理 Quick Search 临时态并聚焦主面板
+- 在设置 > 快捷键新增触控板手势说明,显示第三方工具应调用的 `taglauncher://show`;补齐 29 个语种文案
+- 新增 `Scripts/url_scheme_activation_qa.sh`,覆盖 URL Scheme 注册、show/focus 语义、冷启动 pending、本地化文案和私有手势 API 禁止项
+- 加固 `Scripts/window_logic_qa.sh` 的 Dock 图标断言:本机 Dock 固定正式版 TagLauncher 时,窗口回归只统计当前 QA 构建路径,避免误报重复 Dock 图标
+- 版本号更新为 `8.1.5`,Build 更新为 `20260627.1908`
+
## [8.1.4] — 2026-06-27
- 继续精简 SmartStart 智能初始化默认分类:新用户 / 重置智能初始化方案后的默认标签从 15 个收敛为 13 个
diff --git a/src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md b/src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md
new file mode 100644
index 0000000..25a452c
--- /dev/null
+++ b/src/Docs/Requirements/2026-06-27-pro-theme-preview-countdown-prd.md
@@ -0,0 +1,376 @@
+# PRD:Pro 主题体验倒计时
+
+日期:2026-06-27
+状态:已按 8.1.6 落地并进入 QA 回归
+目标分支:`codex/appgrid-usage-tips`
+目标范围:免费版 + Pro 主题体验
+最低系统:macOS 14.0
+
+## 0. 8.1.6 落地口径
+
+- 到期不新增阻塞弹窗,只退出预览并恢复用户真实可用主题。
+- 当前预览主题卡片只显示紧凑 `MM:SS`,不显示额外“体验中”字样。
+- 倒计时文案不写死 5 分钟,从 `ProThemePreviewState.endsAt` 推导真实剩余时间。
+- 29 个语言文件新增 `pro.theme.status.previewCountdown`,统一保留 `%time%` 占位符。
+
+## 1. 摘要
+
+为 Pro 主题的免费体验状态增加可见倒计时。
+
+当前免费用户可以点击 Pro 专属主题并临时体验该主题。这个体验有时间限制,但界面没有清楚告诉用户还剩多久。体验结束后,App Grid 会回到用户真实可用的主题;如果用户不知道这是限时体验,很容易误以为主题保存失败、设置丢失,或者 Pro 权益逻辑不稳定。
+
+本需求要求在现有主题页界面中增加一个轻量倒计时。倒计时必须以当前真实体验时长为准。当前已核实的代码基线是:每次 Pro 主题体验 5 分钟。如果后续产品决定调整体验时长,倒计时必须自动反映真实剩余时间,不允许在文案里写死“5 分钟”。
+
+## 2. 当前产品事实
+
+当前产品行为如下:
+
+- 免费主题:默认、深蓝、黑色。
+- Pro 主题:粉色、紫色、绿色、蓝色、炫彩。
+- 免费用户点击 Pro 主题后,可以进入临时体验。
+- 体验中的 Pro 主题不会写入用户真实保存的主题设置。
+- 当前体验时长是 5 分钟。
+- 体验中点击另一个 Pro 主题,会开始一轮新的体验。
+- 体验中点击免费主题,会退出体验并保存该免费主题。
+- 体验中购买或恢复 Pro 成功后,当前体验主题应成为用户真实保存的主题。
+
+给研发定位用的当前代码事实:
+
+- 体验时长当前定义为 `5 * 60`。
+- QA 可以通过现有 Pro QA override 缩短体验时长。
+- 主题页已经有轻量 Pro 状态行。
+- 正在体验的 Pro 主题卡片已经有预览标识。
+
+## 3. 问题
+
+当前界面只告诉用户“正在体验 Pro 主题”,但没有告诉用户体验会持续多久、何时结束。
+
+这会带来三个用户可感知问题:
+
+1. 用户无法在体验结束前形成正确预期。
+2. 用户可能把自动恢复误判为 bug 或设置保存失败。
+3. Pro 转化语境变弱,因为用户没有明确理解“这是临时开放给你体验”。
+
+## 4. 产品目标
+
+### 4.1 用户目标
+
+免费用户应该明确知道:
+
+- 自己正在体验 Pro 主题。
+- 这次体验是临时的。
+- 当前还剩多少时间。
+- 解锁 Pro 后可以继续保留该主题。
+
+### 4.2 产品目标
+
+产品应做到:
+
+- 让 Pro 主题体验状态自解释。
+- 降低体验结束时的困惑。
+- 保持主题页克制、紧凑、原生。
+- 不增加重营销界面,不增加额外决策步骤。
+
+## 5. 不做范围
+
+本需求不包含:
+
+- 不把体验时长从 5 分钟改为 15 分钟。
+- 不在开始体验前新增弹窗。
+- 不在体验结束时新增阻塞弹窗。
+- 不增加进度条、环形计时器、滑杆或大型倒计时组件。
+- 不改变 Pro 购买流程。
+- 不改变免费主题列表。
+- 不改变哪些主题属于 Pro。
+- 不改变免费用户的主题持久化规则。
+- 不改变 App Grid 主题视觉。
+- 不改变容器玻璃、App 图标、布局、拖拽、备注、排序、导入或导出行为。
+
+如果产品决定调整体验时长,应作为独立产品决策处理。本 PRD 只要求倒计时反映真实配置的体验剩余时间。
+
+## 6. 交互需求
+
+### 6.1 主题页状态行
+
+当免费用户正在体验 Pro 主题时,现有主题页 Pro 状态行必须显示剩余时间。
+
+推荐中文显示:
+
+```text
+体验中 · 剩余 04:59
+```
+
+推荐英文显示:
+
+```text
+Previewing · 04:59 left
+```
+
+状态行仍应保留 Pro 价值提示,但倒计时必须直接可见,不能依赖 hover、tooltip 或二级面板。
+
+可接受的紧凑中文备选:
+
+```text
+Pro 主题体验中 · 04:59
+```
+
+```text
+剩余 04:59 · 解锁 Pro 后可永久保留
+```
+
+### 6.2 当前体验主题卡片
+
+正在体验的 Pro 主题卡片,必须在现有 accessory 区域显示紧凑时间提示。
+
+优先显示:
+
+```text
+04:59
+```
+
+如果空间允许,可显示:
+
+```text
+体验中 04:59
+```
+
+倒计时只作用于当前正在体验的 Pro 主题卡片。其它锁定的 Pro 主题继续显示普通 Pro 标识。
+
+### 6.3 时间格式
+
+倒计时格式必须稳定、易扫读:
+
+- 体验剩余时间小于 1 小时时,使用 `MM:SS`。
+- 秒数必须补齐两位。
+- 示例:`05:00`、`04:59`、`01:05`、`00:09`。
+- 不显示小数秒。
+- 不显示负数时间。
+- 到期后应退出体验状态,不能长期停留在 `00:00`。
+
+以当前 5 分钟基线看,`MM:SS` 足够。如果未来产品把体验时长提高到 59 分钟以上,需另行决定是否改用小时格式。
+
+### 6.4 开始体验
+
+免费用户点击 Pro 主题时:
+
+1. 立即开始主题体验。
+2. App Grid 切换为该 Pro 主题效果。
+3. 主题页状态行进入体验状态。
+4. 被选中的 Pro 主题卡片显示倒计时。
+5. 倒计时从真实配置体验时长开始。
+
+当前基线下,初始可见值应接近 `05:00`。
+
+### 6.5 体验中切换 Pro 主题
+
+免费用户已经在体验 Pro 主题 A,此时点击 Pro 主题 B:
+
+1. 体验主题切换为 B。
+2. 倒计时为 B 重新开始。
+3. A 的主题卡片恢复普通 Pro 锁定状态。
+4. B 的主题卡片显示倒计时。
+
+界面上不能同时出现两个主题卡片倒计时。
+
+### 6.6 体验中选择免费主题
+
+免费用户体验 Pro 主题时选择免费主题:
+
+1. 体验立即结束。
+2. 被选择的免费主题成为真实保存主题。
+3. 倒计时消失。
+4. 主题页恢复普通免费状态。
+
+不需要警告弹窗。
+
+### 6.7 体验到期
+
+倒计时结束时:
+
+1. Pro 主题体验自动结束。
+2. App Grid 恢复到用户真实可用主题。
+3. 倒计时消失。
+4. 主题页恢复普通免费状态。
+
+推荐轻量反馈:
+
+```text
+主题体验已结束
+```
+
+如果产品内已有轻量 inline 或 toast 反馈模式,可以复用。不要为到期新增阻塞弹窗。
+
+### 6.8 体验中购买或恢复 Pro
+
+用户在体验 Pro 主题时购买或恢复 Pro 成功:
+
+1. 当前体验主题成为真实保存主题。
+2. 倒计时消失。
+3. 主题页进入已解锁状态。
+4. Pro 主题卡片显示选中状态,而不是计时器。
+
+用户不应在成功解锁后丢失正在体验的主题。
+
+### 6.9 关闭与重新打开界面
+
+倒计时应表示真实剩余时间,而不是界面可见时间。
+
+用户可感知行为要求:
+
+- 设置窗口关闭时,如果体验仍在进行,体验计时继续按现有规则运行。
+- 用户在到期前重新打开设置页,倒计时显示当前真实剩余时间。
+- 如果体验在设置页关闭期间到期,重新打开设置页后显示普通免费状态。
+- 如果 App 退出并重启,除非现有体验系统已经支持恢复,否则本需求不要求恢复上次体验。
+
+## 7. 信息架构
+
+本功能归属:
+
+```text
+设置
+└── 主题
+ ├── Pro 状态行
+ │ └── 体验倒计时
+ └── 主题网格
+ └── 当前 Pro 体验主题卡片倒计时
+```
+
+这是 Pro 主题体验的子行为,不是新的设置分区,也不是新的 Pro 管理页。
+
+## 8. 用户旅程
+
+### 8.1 首次体验
+
+1. 免费用户打开“设置 > 主题”。
+2. 用户点击一个 Pro 主题。
+3. App Grid 切换到该主题。
+4. 主题页显示 `体验中 · 剩余 05:00`。
+5. 当前 Pro 主题卡片显示 `05:00`。
+6. 用户知道这是临时体验。
+
+### 8.2 体验结束
+
+1. 用户正在体验 Pro 主题。
+2. 倒计时到期。
+3. App Grid 恢复到用户真实可用主题。
+4. 主题页恢复免费状态文案。
+5. 用户可看到轻量提示:主题体验已结束。
+
+### 8.3 体验中解锁 Pro
+
+1. 免费用户正在体验 Pro 主题。
+2. 用户点击解锁 Pro。
+3. 购买或恢复成功。
+4. 当前体验主题成为真实保存主题。
+5. 倒计时消失,界面显示已解锁状态。
+
+### 8.4 体验中切换主题
+
+1. 免费用户正在体验 Pro 主题 A。
+2. A 上显示倒计时。
+3. 用户点击 Pro 主题 B。
+4. App Grid 切换到 B。
+5. 倒计时在 B 上重新开始。
+6. A 恢复普通 Pro 锁定状态。
+
+## 9. 用户故事
+
+- 作为免费用户,我想看到 Pro 主题体验还剩多久,以便理解为什么主题之后会消失。
+- 作为免费用户,我想清楚知道当前正在体验哪一个主题,以便比较不同 Pro 主题。
+- 作为免费用户,我想在切换另一个 Pro 主题时看到新的倒计时,以便知道体验已经重新开始。
+- 作为潜在购买用户,我希望解锁 Pro 后继续保留正在体验的主题,以便购买前后的体验是连续的。
+- 作为 QA,我希望能用缩短体验时长的方式验证倒计时,以便不用每次等待完整 5 分钟。
+
+## 10. 文案需求
+
+需要覆盖以下含义:
+
+| 含义 | 中文草案 | 英文草案 |
+|:---|:---|:---|
+| 带剩余时间的体验状态 | `体验中 · 剩余 %@` | `Previewing · %@ left` |
+| 卡片紧凑计时 | `%@` | `%@` |
+| 体验结束反馈 | `主题体验已结束` | `Theme preview ended` |
+| 解锁后保留提示 | `解锁 Pro 后可永久保留` | `Unlock Pro to keep it` |
+
+说明:
+
+- `%@` 表示格式化后的剩余时间,例如 `04:59`。
+- 现有 Pro 主题体验文案可以复用,前提是状态足够清楚。
+- 所有支持语言必须补齐本功能文案。
+- 非中文、非英文语言不能混入中文或英文兜底文案。
+
+## 11. 验收标准
+
+PTC-01:免费用户点击任一 Pro 专属主题后,开始体验,并在主题页状态行看到倒计时。
+
+PTC-02:免费用户正在体验的 Pro 主题卡片,在 accessory 区域显示紧凑倒计时。
+
+PTC-03:倒计时以 `MM:SS` 显示真实剩余时间,且不会显示负数。
+
+PTC-04:倒计时必须读取真实配置体验时长,不允许把界面文案写死为“5 分钟”或任何固定时长。
+
+PTC-05:体验中从一个 Pro 主题切换到另一个 Pro 主题时,新主题重新开始倒计时,旧主题移除倒计时。
+
+PTC-06:体验中选择免费主题时,立即结束体验、隐藏倒计时,并保存该免费主题。
+
+PTC-07:体验时间到期后,界面退出体验状态、隐藏倒计时,并恢复用户真实可用主题;不得弹出阻塞弹窗。
+
+PTC-08:体验中解锁 Pro 成功后,当前体验主题成为真实保存主题,倒计时消失。
+
+PTC-09:体验中关闭并重新打开设置页,如果体验尚未到期,应显示真实剩余时间。
+
+PTC-10:体验在设置页关闭期间到期后,重新打开设置页不得显示过期倒计时。
+
+PTC-11:Pro 用户和 legacy Pro 用户不显示 Pro 主题倒计时,按普通已解锁主题行为展示。
+
+PTC-12:所有支持语言都必须补齐倒计时和体验结束文案,不得出现占位符泄漏或混合语言兜底。
+
+PTC-13:倒计时每秒变化时,不得造成主题卡片宽高跳动或主题网格布局抖动。
+
+PTC-14:倒计时在浅色和深色系统外观下都必须清晰可读。
+
+PTC-15:QA 可以通过现有缩短体验时长的 QA 路径验证倒计时到期行为。
+
+## 12. QA 范围
+
+必须覆盖以下场景:
+
+1. 免费用户分别体验每个 Pro 主题。
+2. 免费用户在两个 Pro 主题之间切换。
+3. 免费用户从 Pro 主题体验切换到免费主题。
+4. 免费用户等待体验到期。
+5. 免费用户体验中关闭并重新打开设置页。
+6. 免费用户关闭设置页后等待体验到期,再重新打开设置页。
+7. 免费用户体验中购买或恢复 Pro。
+8. Pro 用户选择 Pro 主题。
+9. legacy Pro 用户选择 Pro 主题。
+10. 29 种语言文案完整性。
+11. 浅色和深色系统外观。
+12. macOS 14 兼容性。
+
+推荐 QA 加速方式:
+
+- 使用现有缩短体验时长的 QA override 验证到期链路,不必每次等待完整生产时长。
+- 至少包含一条测试:倒计时从缩短时长开始,进入最后几秒,到期后退出体验,并且界面不残留过期计时。
+
+## 13. 发布说明草案
+
+中文:
+
+```text
+Pro 主题体验现在会显示剩余时间,体验结束前后状态更清楚。
+```
+
+英文:
+
+```text
+Pro theme previews now show remaining time, making temporary previews easier to understand.
+```
+
+## 14. 待确认决策
+
+OD-01:生产体验时长保持当前 5 分钟,还是作为独立产品决策改为其它时长。
+
+OD-02:体验结束时只通过状态行恢复表达,还是额外复用现有轻量 toast。
+
+OD-03:主题卡片 accessory 最终采用纯时间 `04:59`,还是在空间允许时采用 `体验中 04:59`。
diff --git a/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md b/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md
index 81412b3..4bec8e2 100644
--- a/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md
+++ b/src/Docs/Requirements/2026-06-27-smartstart-category-simplification.md
@@ -2,11 +2,11 @@
日期:2026-06-27
-状态:产品已确认 `network-tools` 归入 `System Tools / 系统工具`;确认 `system-tools` 的用户可见名称采用 `System Tools / 系统工具`;确认 `office`、`PDF`、`writing` 合并为 `Office & Docs / 办公文档`;确认 `Video / Audio / Games` 合并进 `Entertainment / 影音娱乐`;确认 `Finance / 财务` 和 `Other / 其他` 回到未分类,最终默认智能分类为 13 个。
+状态:产品已确认 `network-tools` 归入 `System Tools / 系统工具`;确认 `system-tools` 的用户可见名称采用 `System Tools / 系统工具`;确认 `office`、`PDF`、`writing` 合并为 `Office & Docs / 办公文档`;确认 `Video / Audio / Games` 合并进 `Entertainment / 影音娱乐`;确认 `Finance / 财务` 和 `Other / 其他` 回到未分类;确认 `Meeting / 会议` 合并进 `Communication / 沟通`,最终默认智能分类为 12 个。
## 1. 本轮目标
-用户反馈当前智能初始化使用 39 个默认标签,过于冗余。本方案把默认标签精简为 13 个,并明确合并关系、最终标签列表与后续落地注意事项。
+用户反馈当前智能初始化使用 39 个默认标签,过于冗余。本方案把默认标签精简为 12 个,并明确合并关系、最终标签列表与后续落地注意事项。
## 2. 当前 39 个初始化标签
@@ -149,6 +149,16 @@
- `Other / 其他` 本质上不是智能分类结果,而是“无法确定分类”的兜底状态,应该回到未分类。
- 本轮仍不做老用户迁移;已有用户手动创建或历史保留的财务 / 其他标签不自动删除、不改名、不迁移。
+### 改动 7:Meeting / 会议合并为 Communication / 沟通
+
+`Meeting / 会议` 不再作为默认智能初始化标签,归并到 `Communication / 沟通`。
+
+确认理由:
+
+- 会议类 App 本质上属于沟通协作场景,独立标签会放大默认标签冗余。
+- 默认初始化应优先给新用户更少、更稳定的分类心智;如果用户需要单独管理会议工具,可后续手动拆分。
+- 本轮仍不做老用户迁移;已有用户手动创建或历史保留的会议标签不自动删除、不改名、不迁移。
+
## 4. 命名确认
### System Tools
@@ -177,29 +187,28 @@
当前:39 个默认标签。
-合并后:13 个默认标签。
+合并后:12 个默认标签。
-减少:26 个标签。
+减少:27 个标签。
-## 6. 最终 13 个默认标签列表
+## 6. 最终 12 个默认标签列表
以下列表按“尽量保留原始顺序 + 合并项放在首次出现位置”的原则排列。
| 新序号 | category id | 英文显示名 | 简体中文显示名 | 来源 |
|---:|---|---|---|---|
| 1 | `browser` | Browsers | 浏览器 | 原 1 |
-| 2 | `communication` | Communication | 沟通 | 原 2 |
+| 2 | `communication` | Communication | 沟通 | 原 2、5 |
| 3 | `GTD` | Tasks & GTD | 任务与 GTD | 原 3 |
| 4 | `Notes` | Notes | 笔记 | 原 4 |
-| 5 | `Meeting` | Meetings | 会议 | 原 5 |
-| 6 | `office` | Office & Docs | 办公文档 | 原 6、7、21 |
-| 7 | `system-enhancement` | System Tools | 系统工具 | 原 8、9、26、27、28、29、30、31、33、38 |
-| 8 | `ai-tools` | AI Tools | AI 工具 | 原 10 |
-| 9 | `development` | Coding | 编程 | 原 11、12、13、14、15、16、32 |
-| 10 | `design` | Design | 设计 | 原 17、18、19、20 |
-| 11 | `picture-photo` | Photos | 图片 | 原 25 |
-| 12 | `entertainment` | Entertainment | 影音娱乐 | 原 22、23、24、34、35 |
-| 13 | `education` | Education | 学习 | 原 37 |
+| 5 | `office` | Office & Docs | 办公文档 | 原 6、7、21 |
+| 6 | `system-enhancement` | System Tools | 系统工具 | 原 8、9、26、27、28、29、30、31、33、38 |
+| 7 | `ai-tools` | AI Tools | AI 工具 | 原 10 |
+| 8 | `development` | Coding | 编程 | 原 11、12、13、14、15、16、32 |
+| 9 | `design` | Design | 设计 | 原 17、18、19、20 |
+| 10 | `picture-photo` | Photos | 图片 | 原 25 |
+| 11 | `entertainment` | Entertainment | 影音娱乐 | 原 22、23、24、34、35 |
+| 12 | `education` | Education | 学习 | 原 37 |
## 7. 完整合并映射表
@@ -209,7 +218,7 @@
| 2 | `communication` | 沟通 | `communication` | 沟通 |
| 3 | `GTD` | 任务与 GTD | `GTD` | 任务与 GTD |
| 4 | `Notes` | 笔记 | `Notes` | 笔记 |
-| 5 | `Meeting` | 会议 | `Meeting` | 会议 |
+| 5 | `Meeting` | 会议 | `communication` | 沟通 |
| 6 | `office` | 办公 | `office` | 办公文档 |
| 7 | `PDF` | PDF | `office` | 办公文档 |
| 8 | `file-management` | 文件管理 | `system-enhancement` | 系统工具 |
@@ -249,15 +258,16 @@
如果产品确认这版列表,建议后续实现按低风险路径做:
-1. 调整 `SmartCategoryDefaults.orderedIDs`,把 39 个默认标签压缩为 13 个。
+1. 调整 `SmartCategoryDefaults.orderedIDs`,把 39 个默认标签压缩为 12 个。
2. 复用既有 `SmartCategoryID.office`、`SmartCategoryID.development`、`SmartCategoryID.design`、`SmartCategoryID.systemEnhancement` 作为合并目标。
3. 建立旧 category id 到新 category id 的映射表,SmartStart 匹配结果统一归并后再写入标签。
4. 29 个语言包中将 `system-enhancement` 的用户可见文案改为 `System Tools / 系统工具`对应语义,将 `office` 改为 `Office & Docs / 办公文档`对应语义,将 `picture-photo` 简化为 `Photos / 图片`对应语义。
5. QA 覆盖:
- - 新用户智能初始化最终标签数是 13。
+ - 新用户智能初始化最终标签数是 12。
- 合并来源 app 进入正确目标标签。
- 第 33 项 `network-tools`归入 `system-enhancement`,不重复创建标签。
- `office`、`PDF`、`writing`都归入 `Office & Docs / 办公文档`。
+ - `Meeting / 会议`归入 `Communication / 沟通`,不再创建独立默认会议标签。
- `finance` 和 `other` 不生成默认标签,命中后回到未分类。
- 新默认智能分类结果中 29 语种没有旧细分标签残留、没有英文兜底污染。
- 老用户已有标签保持原样;本轮不做旧 39 标签自动迁移。
diff --git a/src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md b/src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md
new file mode 100644
index 0000000..e6fa74b
--- /dev/null
+++ b/src/Docs/Requirements/2026-06-27-trackpad-gesture-integration-prd.md
@@ -0,0 +1,606 @@
+# PRD: Trackpad Gesture Integration for TagLauncher
+
+Date: 2026-06-27
+Status: Implemented in 8.1.5 and included in 8.1.6 release QA
+Owner: Product / macOS platform
+Target area: Hotkeys, external activation, help documentation
+Minimum macOS: macOS 14.0
+
+## 1. Summary
+
+Some Mac users built muscle memory around the old trackpad gesture that opens Launchpad: pinch with thumb and three fingers. On newer macOS versions where the old Launchpad experience is no longer available in the same way, that gesture may no longer produce a useful launcher result for these users.
+
+The product question is whether TagLauncher can bind itself directly to that trackpad gesture.
+
+Decision:
+
+TagLauncher should not attempt to capture or register the system-level trackpad gesture directly. The stable and App Store-safe path is to provide reliable invocation endpoints, then document how advanced users can map a third-party gesture tool to those endpoints.
+
+Recommended product shape:
+
+- TagLauncher provides a first-party invocation endpoint:
+ - `taglauncher://show`
+ - existing CLI handoff can remain an advanced fallback: `--show-overlay`
+- TagLauncher documents third-party gesture mapping:
+ - BetterTouchTool as the primary trackpad-gesture example.
+ - Keyboard Maestro as a macro automation example only if verified.
+ - Hammerspoon as free/open-source automation, but not promised as the primary trackpad gesture solution unless the exact gesture is verified.
+- TagLauncher does not use private multitouch APIs or global gesture interception.
+
+## 2. Background
+
+Existing TagLauncher behavior:
+
+- TagLauncher already has global keyboard shortcuts.
+- TagLauncher already has a menu bar entry.
+- TagLauncher already has a duplicate-instance handoff path:
+ - `src/Apptag/ProcessSingleton.swift`
+ - A second launch with `--show-overlay` posts `TagLauncherExternalActivationRequested`.
+ - The running instance handles it and calls `showOrFocusOverlay()`.
+
+Relevant existing code:
+
+- `src/Apptag/ProcessSingleton.swift:35`
+ - reads process arguments.
+- `src/Apptag/ProcessSingleton.swift:36`
+ - checks `--show-overlay`.
+- `src/Apptag/ProcessSingleton.swift:52`
+ - posts the external activation distributed notification.
+- `src/Apptag/ApptagApp.swift:1733`
+ - observes external activation requests.
+- `src/Apptag/ApptagApp.swift:1742`
+ - handles the activation request.
+- `src/Apptag/ApptagApp.swift:1748`
+ - calls `showOrFocusOverlay()`.
+
+This means the project already has most of the internal activation path needed. The missing product layer is a clean public invocation surface and documentation.
+
+## 3. Product Goals
+
+1. Help users recreate a Launchpad-like trackpad launcher habit with TagLauncher.
+2. Keep TagLauncher App Store-safe and compatible with macOS permission models.
+3. Avoid fragile private APIs and system gesture interception.
+4. Provide a simple supportable setup path for advanced users.
+5. Preserve existing global keyboard shortcut behavior.
+
+## 4. Non-Goals
+
+Do not implement:
+
+- Direct registration of the system Launchpad gesture.
+- Global capture of thumb + three-finger pinch inside TagLauncher.
+- Use of private `MultitouchSupport.framework`.
+- Use of undocumented IOKit multitouch device parsing.
+- A background gesture daemon inside TagLauncher.
+- Accessibility-required raw gesture capture in the main app.
+- A default feature that asks every user to grant Accessibility/Input Monitoring permissions.
+- Replacing system Trackpad settings.
+- App Store review-risky gesture interception.
+
+## 5. Technical Position
+
+### 5.1 Why TagLauncher Should Not Capture the Gesture Directly
+
+macOS does not provide a public, App Store-friendly API that lets a third-party app claim the system-level thumb + three-finger pinch gesture globally.
+
+AppKit gesture events are designed for the active application and its responder/view hierarchy. They are not a stable registration mechanism for background launcher apps.
+
+Low-level event taps are not a clean solution for this specific system gesture:
+
+- They are not a public "register global trackpad gesture" API.
+- They may require additional privacy permissions.
+- They can be brittle across macOS versions.
+- They are hard to explain to users.
+- They introduce App Store review and maintenance risk.
+
+Private multitouch frameworks or raw device parsing are explicitly out of scope.
+
+### 5.2 Recommended Integration Model
+
+Use this chain:
+
+```text
+User trackpad gesture
+-> third-party gesture tool
+-> TagLauncher public invocation endpoint
+-> existing showOrFocusOverlay path
+```
+
+TagLauncher owns:
+
+- Stable invocation endpoint.
+- Documentation.
+- Settings-page guidance.
+- QA for invocation behavior.
+
+Third-party tools own:
+
+- Trackpad gesture capture.
+- Per-user gesture mapping.
+- Permissions required for gesture capture.
+
+## 6. Recommended Product Changes
+
+### 6.1 Add URL Scheme
+
+Add a custom URL scheme:
+
+```text
+taglauncher://show
+```
+
+Expected behavior:
+
+- If TagLauncher is already running, show/focus App Grid overlay.
+- If TagLauncher is not running, launch TagLauncher and show App Grid overlay.
+- If overlay is already visible, keep or focus it; do not create duplicate windows.
+
+Recommended future URL, optional:
+
+```text
+taglauncher://quick-search
+```
+
+Not required for first release unless product wants a separate Quick Search gesture.
+
+### 6.2 Keep CLI Handoff as Advanced Fallback
+
+Existing internal path:
+
+```text
+TagLauncher --show-overlay
+```
+
+For advanced documentation, the macOS `open` command can call the app with args:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+Do not make this the primary user-facing recommendation if URL Scheme exists. URL Scheme is simpler for external tools.
+
+### 6.3 Add Help Documentation
+
+Create a Help section:
+
+Title:
+
+```text
+Use a Trackpad Gesture to Open TagLauncher
+```
+
+Chinese title:
+
+```text
+用触摸板手势打开 TagLauncher
+```
+
+Core message:
+
+```text
+macOS does not let TagLauncher directly claim the system Launchpad gesture. To use a trackpad gesture, map the gesture in a third-party automation tool to taglauncher://show or to your TagLauncher global shortcut.
+```
+
+Chinese draft:
+
+```text
+macOS 不允许 TagLauncher 直接接管系统级启动台手势。你可以使用第三方自动化工具,把触摸板手势映射到 taglauncher://show,或映射到 TagLauncher 的全局快捷键。
+```
+
+### 6.4 Add Settings Copy
+
+In Settings > Hotkeys, add a small help note or help button.
+
+Recommended text:
+
+```text
+Want to open TagLauncher with a trackpad gesture? Map your gesture to taglauncher://show using a gesture utility.
+```
+
+Chinese draft:
+
+```text
+想用触摸板手势唤出 TagLauncher?可用手势工具将手势映射到 taglauncher://show。
+```
+
+Keep this as secondary help text. Do not make it a large settings section.
+
+## 7. Third-Party Tool Guidance
+
+### 7.1 Recommended Tool Positioning
+
+Primary documented tool:
+
+- BetterTouchTool
+ - Strong fit for trackpad gestures.
+ - Commercial.
+ - Good for non-technical users who explicitly want gesture mapping.
+
+Mention with caution:
+
+- Keyboard Maestro
+ - Commercial macro tool.
+ - Useful if it can be configured for the target gesture or for another input trigger.
+ - Must be verified before official step-by-step docs claim support for this exact gesture.
+
+Open-source/free option:
+
+- Hammerspoon
+ - Free and open-source.
+ - Good for technical users.
+ - Strong for hotkey/URL automation.
+ - Do not promise reliable thumb + three-finger pinch capture unless QA verifies that exact gesture path on target macOS versions.
+
+### 7.2 Recommended User-Facing Wording
+
+Avoid:
+
+```text
+TagLauncher supports the Launchpad gesture.
+```
+
+Use:
+
+```text
+You can map a trackpad gesture to TagLauncher using a gesture utility.
+```
+
+Chinese:
+
+```text
+你可以使用手势工具,把触摸板手势映射到 TagLauncher。
+```
+
+This avoids implying TagLauncher owns or overrides system gestures.
+
+## 8. Implementation Requirements
+
+### 8.1 URL Scheme Registration
+
+Add `CFBundleURLTypes` to `Info.plist`.
+
+Suggested scheme:
+
+```text
+taglauncher
+```
+
+Supported routes:
+
+```text
+taglauncher://show
+```
+
+Optional later:
+
+```text
+taglauncher://quick-search
+taglauncher://settings/hotkeys
+```
+
+### 8.2 URL Handling
+
+Implement URL handling in the AppDelegate path.
+
+Expected AppKit delegate method:
+
+```swift
+func application(_ application: NSApplication, open urls: [URL])
+```
+
+For `taglauncher://show`:
+
+- Dismiss conflicting transient state if needed.
+- Call existing `showOrFocusOverlay()` path.
+- Respect current fullscreen/Split View overlay behavior.
+- Do not create duplicate windows.
+
+For unknown URL:
+
+- Ignore safely.
+- Optionally log diagnostics.
+
+### 8.3 Cold Launch Behavior
+
+If TagLauncher is not running and user opens:
+
+```text
+taglauncher://show
+```
+
+Expected:
+
+1. App launches.
+2. Defaults and localization initialize normally.
+3. Overlay shows after app is ready.
+4. No duplicate Dock icon is created.
+5. No settings window is opened.
+
+Implementation note:
+
+If URL handling arrives before the app is fully initialized, queue a pending `showOverlay` request and execute it after `applicationDidFinishLaunching`.
+
+### 8.4 Running Instance Behavior
+
+If TagLauncher is already running:
+
+- `taglauncher://show` should route to the existing instance.
+- The app should show/focus overlay exactly like the menu bar command or global hotkey.
+- If overlay is already visible, behavior should match existing toggle/focus policy decided by product.
+
+Recommended:
+
+- `taglauncher://show` should show/focus, not toggle-hide.
+
+Reason:
+
+External gesture tools may send repeated or delayed activations. A deterministic "show" command is safer than toggle.
+
+### 8.5 CLI Compatibility
+
+Keep `--show-overlay` working.
+
+Add tests or QA steps for:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+This gives power users and automation tools a fallback if URL Scheme is unavailable.
+
+## 9. UX Requirements
+
+### 9.1 Settings > Hotkeys
+
+Add a small secondary entry under the existing hotkey information:
+
+- Title: `Trackpad gesture`
+- Body: `Use a gesture utility to map a trackpad gesture to taglauncher://show.`
+- Optional action: `Open Help`
+
+Chinese draft:
+
+- Title: `触摸板手势`
+- Body: `可用手势工具将触摸板手势映射到 taglauncher://show。`
+- Action: `查看帮助`
+
+Do not add:
+
+- Gesture recorder.
+- Permission prompt.
+- System Trackpad settings clone.
+- Tool-specific configuration UI.
+
+### 9.2 Help Document
+
+Help document should include:
+
+1. Why TagLauncher cannot directly claim the system gesture.
+2. Recommended approach using a gesture utility.
+3. BetterTouchTool example.
+4. Generic URL Scheme example:
+
+```text
+taglauncher://show
+```
+
+5. CLI fallback:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+6. Testing checklist:
+
+- Normal desktop.
+- Fullscreen app.
+- Split View.
+- External display.
+
+### 9.3 BetterTouchTool Example Flow
+
+Document only after verifying labels in the current BetterTouchTool UI.
+
+Intended flow:
+
+1. Open BetterTouchTool.
+2. Choose Trackpad.
+3. Add a gesture.
+4. Select thumb + three-finger pinch, or the closest available gesture.
+5. Set action to open URL:
+
+```text
+taglauncher://show
+```
+
+6. Test on normal desktop.
+7. Test in fullscreen and Split View.
+
+If BetterTouchTool cannot open URL directly in the selected action type, use command fallback:
+
+```bash
+open "taglauncher://show"
+```
+
+or:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+## 10. Privacy and Permissions
+
+TagLauncher should not request new permissions for this feature.
+
+Expected:
+
+- TagLauncher does not request Accessibility permission for gesture capture.
+- TagLauncher does not request Input Monitoring.
+- Third-party gesture tools may request their own permissions.
+- Help text must make it clear those permissions belong to the external tool, not TagLauncher.
+
+This keeps TagLauncher simpler for App Store review and user trust.
+
+## 11. QA Requirements
+
+### 11.1 TagLauncher Endpoint QA
+
+Test `taglauncher://show`:
+
+- App not running.
+- App running in menu bar only.
+- App running with overlay hidden.
+- Overlay already visible.
+- Settings window open.
+- Quick Search open.
+- Fullscreen Space.
+- Split View.
+- Multiple displays if available.
+
+Expected:
+
+- Overlay shows or focuses.
+- No duplicate visible instance.
+- No Dock icon flicker beyond existing behavior.
+- No Space jump regression.
+- No broken Quick Search session.
+
+### 11.2 CLI Fallback QA
+
+Test:
+
+```bash
+open -na "TagLauncher" --args --show-overlay
+```
+
+Scenarios:
+
+- App not running.
+- App already running.
+- App hidden as menu bar/accessory app.
+- Fullscreen Space.
+- Split View.
+
+Expected:
+
+- Existing handoff path works.
+- Existing app receives request.
+- No duplicate instance remains.
+
+### 11.3 Help Documentation QA
+
+Verify:
+
+- Instructions do not claim native gesture support.
+- Instructions do not claim Hammerspoon supports the exact gesture unless verified.
+- BetterTouchTool steps match the actual current UI before release.
+- Paid/commercial nature of third-party tools is not hidden if mentioned.
+- Hammerspoon is described accurately as free/open-source but technical.
+
+### 11.4 Optional Third-Party Smoke QA
+
+If BetterTouchTool is available:
+
+- Configure thumb + three-finger pinch gesture.
+- Bind it to `taglauncher://show`.
+- Test normal desktop, fullscreen, Split View.
+
+If Hammerspoon is evaluated:
+
+- Verify whether the exact trackpad gesture is actually capturable.
+- If not reliable, do not publish Hammerspoon as the trackpad-pinch recipe.
+
+## 12. Acceptance Criteria
+
+The feature is accepted when:
+
+1. `taglauncher://show` is registered and works.
+2. Cold launch through `taglauncher://show` opens TagLauncher overlay.
+3. Running-instance invocation through `taglauncher://show` focuses/shows the existing overlay.
+4. `--show-overlay` remains working.
+5. No new TagLauncher privacy permission is required.
+6. Settings copy is short and non-invasive.
+7. Help documentation clearly states that gesture capture is handled by third-party tools.
+8. QA confirms fullscreen/Split View behavior remains stable.
+9. App Store review risk from private gesture APIs is avoided.
+
+## 13. Rejected Alternatives
+
+### 13.1 Native Global Gesture Registration
+
+Rejected.
+
+Reason:
+
+No stable public API exists for a third-party app to claim the system Launchpad gesture globally.
+
+### 13.2 Raw Multitouch Capture
+
+Rejected.
+
+Reason:
+
+Requires private or brittle implementation paths, creates permission and App Store risk, and is likely to break across macOS releases.
+
+### 13.3 Built-In Gesture Recorder
+
+Rejected for first release.
+
+Reason:
+
+Would imply TagLauncher owns gesture capture, increase complexity, and create user-support burden.
+
+### 13.4 Make BetterTouchTool a Hard Dependency
+
+Rejected.
+
+Reason:
+
+TagLauncher should remain useful without external tools. BetterTouchTool can be documented as an optional integration, not a dependency.
+
+## 14. Open Questions
+
+1. Should the first release include only `taglauncher://show`, or also `taglauncher://quick-search`?
+2. Should Settings > Hotkeys include an `Open Help` button, or only static help text?
+3. Should the help document mention BetterTouchTool by name, or use generic "gesture utility" wording plus examples?
+4. Should the App Store version include the same help text as direct-download builds?
+
+Recommended answers:
+
+1. First release only `taglauncher://show`.
+2. Include `Open Help` if the help page exists; otherwise static text.
+3. Use generic wording first, then examples.
+4. Yes, because TagLauncher itself is not doing gesture capture.
+
+## 15. References
+
+Apple:
+
+- Mac trackpad gestures:
+ https://support.apple.com/en-us/102482
+- Handling trackpad events:
+ https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/EventOverview/HandlingTouchEvents/HandlingTouchEvents.html
+- Defining a custom URL scheme:
+ https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app
+
+External tools:
+
+- BetterTouchTool:
+ https://folivora.ai
+- Keyboard Maestro:
+ https://www.keyboardmaestro.com
+- Hammerspoon:
+ https://www.hammerspoon.org
+ https://github.com/Hammerspoon/hammerspoon
+
+## 16. Final Recommendation
+
+Do not build native trackpad gesture capture into TagLauncher.
+
+Build and document a robust invocation endpoint instead:
+
+```text
+taglauncher://show
+```
+
+Then let users map trackpad gestures to that endpoint with their preferred gesture utility. This is the lowest-risk path for macOS 26 compatibility, App Store review, privacy permissions, and long-term maintenance.
diff --git a/src/Scripts/pro_feature_gate_qa.sh b/src/Scripts/pro_feature_gate_qa.sh
index ce07fc2..5ce6ff1 100755
--- a/src/Scripts/pro_feature_gate_qa.sh
+++ b/src/Scripts/pro_feature_gate_qa.sh
@@ -128,6 +128,13 @@
require(preferences, "presentSettingsProPrompt(for: .premiumThemes)", "premium theme selection prompt is missing")
require(preferences, "proEntitlement.startThemePreview(for: theme)", "premium theme free preview is missing")
require(preferences, "proEntitlement.stopThemePreview()", "theme preview cleanup is missing")
+require(pro, "func remainingSeconds(at date: Date = Date()) -> Int", "theme preview remaining-seconds helper is missing")
+require(pro, "ceil(endsAt.timeIntervalSince(date))", "theme preview countdown must derive from endsAt")
+require(preferences, "pro.theme.status.previewCountdown", "theme preview status countdown localization is missing")
+require(preferences, "formatThemePreviewCountdown(seconds:", "theme preview countdown formatter is missing")
+require(preferences, ".countdown(text: themePreviewCountdownText ?? \"00:00\")", "current preview theme card must show countdown")
+require(preferences, ".onReceive(themePreviewCountdownTimer)", "theme preview countdown UI timer is missing")
+require(preferences, ".font(.system(size: 10, weight: .semibold, design: .monospaced))", "theme preview card countdown must use monospaced digits")
require(content, "@ObservedObject private var proEntitlement = ProEntitlementCenter.shared", "ContentView does not observe Pro entitlement")
require(content, "ProEntitlementPolicy.effectiveTheme(for: appGridTheme)", "ContentView does not apply effective Pro theme")
diff --git a/src/Scripts/pro_localization_qa.sh b/src/Scripts/pro_localization_qa.sh
index 1555425..3501623 100755
--- a/src/Scripts/pro_localization_qa.sh
+++ b/src/Scripts/pro_localization_qa.sh
@@ -20,6 +20,7 @@
}
REQUIRED_PLACEHOLDERS = {
"pro.notes.remaining": {"%limit%", "%remaining%"},
+ "pro.theme.status.previewCountdown": {"%time%"},
}
diff --git a/src/Scripts/pro_theme_preview_countdown_qa.sh b/src/Scripts/pro_theme_preview_countdown_qa.sh
new file mode 100755
index 0000000..255be3b
--- /dev/null
+++ b/src/Scripts/pro_theme_preview_countdown_qa.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+
+python3 - "$ROOT_DIR" <<'PY'
+import json
+import re
+import sys
+from pathlib import Path
+
+root = Path(sys.argv[1])
+app = root / "Apptag"
+preferences = app / "PreferencesView.swift"
+pro = app / "ProEntitlement.swift"
+l10n = app / "L10n.swift"
+localization_dir = app / "Localization"
+
+
+def fail(message: str) -> None:
+ raise SystemExit(f"FAIL: {message}")
+
+
+def read(path: Path) -> str:
+ try:
+ return path.read_text(encoding="utf-8")
+ except Exception as error:
+ fail(f"could not read {path}: {error}")
+
+
+def function_body(source: str, signature: str) -> str:
+ start = source.find(signature)
+ if start == -1:
+ fail(f"missing function signature: {signature}")
+ brace = source.find("{", start)
+ if brace == -1:
+ fail(f"missing function body for: {signature}")
+ depth = 0
+ for index in range(brace, len(source)):
+ char = source[index]
+ if char == "{":
+ depth += 1
+ elif char == "}":
+ depth -= 1
+ if depth == 0:
+ return source[brace + 1:index]
+ fail(f"unterminated function body for: {signature}")
+
+
+preferences_text = read(preferences)
+pro_text = read(pro)
+l10n_text = read(l10n)
+
+required_pro = [
+ "func remainingSeconds(at date: Date = Date()) -> Int",
+ "ceil(endsAt.timeIntervalSince(date))",
+ "max(0, Int(",
+]
+missing_pro = [needle for needle in required_pro if needle not in pro_text]
+if missing_pro:
+ fail("ProThemePreviewState countdown helper is incomplete: " + " | ".join(missing_pro))
+
+required_preferences = [
+ "themePreviewCountdownTimer",
+ "@State private var themePreviewNow = Date()",
+ "pro.theme.status.previewCountdown",
+ ".replacingOccurrences(of: \"%time%\", with: countdown)",
+ ".countdown(text: themePreviewCountdownText ?? \"00:00\")",
+ "formatThemePreviewCountdown(seconds:",
+ "String(format: \"%02d:%02d\"",
+ "proEntitlement.stopThemePreview()",
+ ".font(.system(size: 10, weight: .semibold, design: .monospaced))",
+]
+missing_preferences = [needle for needle in required_preferences if needle not in preferences_text]
+if missing_preferences:
+ fail("Preferences countdown UI is incomplete: " + " | ".join(missing_preferences))
+
+accessory_body = function_body(preferences_text, "private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory")
+if "pro.card.previewBadge" in accessory_body:
+ fail("current preview theme card must show MM:SS, not the old Preview badge")
+
+codes_match = re.search(
+ r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]",
+ l10n_text,
+ re.S,
+)
+if not codes_match:
+ fail("could not find L10n.supported")
+codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', codes_match.group(1))
+if len(codes) != 29:
+ fail(f"expected 29 languages, got {len(codes)}")
+
+for code in codes:
+ path = localization_dir / f"{code}.json"
+ if not path.exists():
+ fail(f"missing localization file: {path}")
+ data = json.loads(path.read_text(encoding="utf-8"))
+ value = data.get("pro.theme.status.previewCountdown", "")
+ if not isinstance(value, str) or not value.strip():
+ fail(f"{path.name} missing pro.theme.status.previewCountdown")
+ if "%time%" not in value:
+ fail(f"{path.name} countdown copy lost %time% placeholder")
+ if "5" in value or "five" in value.lower():
+ fail(f"{path.name} countdown copy must not hardcode preview duration: {value!r}")
+
+print("PASS Pro theme preview countdown QA: UI timer, MM:SS card accessory, expiry cleanup, and 29-language placeholder coverage are wired")
+PY
diff --git a/src/Scripts/smartstart_default_categories_qa.sh b/src/Scripts/smartstart_default_categories_qa.sh
index 2c790b4..33c463f 100755
--- a/src/Scripts/smartstart_default_categories_qa.sh
+++ b/src/Scripts/smartstart_default_categories_qa.sh
@@ -43,7 +43,6 @@
"communication",
"gtd",
"notes",
- "meeting",
"office",
"systemEnhancement",
"aiTools",
@@ -59,7 +58,6 @@
"communication",
"GTD",
"Notes",
- "Meeting",
"office",
"system-enhancement",
"ai-tools",
@@ -73,6 +71,7 @@
DROP_RAW_IDS = {"finance", "other"}
MERGE_MAP = {
+ "Meeting": "communication",
"PDF": "office",
"productivity": "office",
"writing": "office",
@@ -150,6 +149,8 @@
fail(f"SmartCategoryDefaults.orderedIDs mismatch: {ordered_cases}")
required_mapping_snippets = [
+ "case .meeting:",
+ "return .communication",
"case .pdf, .productivity, .writing:",
"return .office",
"case .fileManagement, .transfer, .utilities, .system, .systemMaintenance,",
@@ -208,6 +209,7 @@
print(
"PASS SmartStart default categories: "
f"{len(EXPECTED_ORDERED_CASES)} defaults, catalog IDs normalized, "
- "finance/other uncategorized, starter tags restricted, target labels checked"
+ "Meeting merged into communication, finance/other uncategorized, "
+ "starter tags restricted, target labels checked"
)
PY
diff --git a/src/Scripts/url_scheme_activation_qa.sh b/src/Scripts/url_scheme_activation_qa.sh
new file mode 100755
index 0000000..b4e0130
--- /dev/null
+++ b/src/Scripts/url_scheme_activation_qa.sh
@@ -0,0 +1,134 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+INFO_PLIST="$ROOT_DIR/Apptag/Info.plist"
+APPTAG_APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift"
+PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
+L10N_SWIFT="$ROOT_DIR/Apptag/L10n.swift"
+LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
+
+fail() {
+ printf 'FAIL: %s\n' "$*" >&2
+ exit 1
+}
+
+[[ -f "$INFO_PLIST" ]] || fail "missing Info.plist"
+[[ -f "$APPTAG_APP_SWIFT" ]] || fail "missing ApptagApp.swift"
+[[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift"
+[[ -f "$L10N_SWIFT" ]] || fail "missing L10n.swift"
+[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
+
+plutil -lint "$INFO_PLIST" >/dev/null || fail "Info.plist is not valid"
+
+SCHEME=$(/usr/libexec/PlistBuddy -c "Print :CFBundleURLTypes:0:CFBundleURLSchemes:0" "$INFO_PLIST" 2>/dev/null || true)
+[[ "$SCHEME" == "taglauncher" ]] || fail "Info.plist must register taglauncher URL scheme"
+
+VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$INFO_PLIST")
+BUILD=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$INFO_PLIST")
+[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || fail "invalid CFBundleShortVersionString: $VERSION"
+[[ "$BUILD" =~ ^[0-9]{8}\.[0-9]{4}$ ]] || fail "invalid CFBundleVersion: $BUILD"
+
+python3 - "$ROOT_DIR" <<'PY'
+import json
+import pathlib
+import re
+import sys
+
+root = pathlib.Path(sys.argv[1])
+app = (root / "Apptag" / "ApptagApp.swift").read_text(encoding="utf-8")
+preferences = (root / "Apptag" / "PreferencesView.swift").read_text(encoding="utf-8")
+l10n = (root / "Apptag" / "L10n.swift").read_text(encoding="utf-8")
+localization_dir = root / "Apptag" / "Localization"
+
+
+def fail(message: str) -> None:
+ print(f"FAIL: {message}", file=sys.stderr)
+ sys.exit(1)
+
+
+def require(pattern: str, source: str, message: str) -> None:
+ if not re.search(pattern, source, re.S):
+ fail(message)
+
+
+require(
+ r"func\s+application\s*\(\s*_\s+application\s*:\s*NSApplication\s*,\s*open\s+urls\s*:\s*\[URL\]\s*\)",
+ app,
+ "AppDelegate must implement application(_:open:) for URL scheme activation",
+)
+require(r'externalInvocationScheme\s*=\s*"taglauncher"', app, "URL scheme constant must be taglauncher")
+require(r'externalInvocationShowHost\s*=\s*"show"', app, "URL show route constant must be show")
+require(r"pendingShowOverlayInvocation", app, "cold-launch URL requests must have a pending show flag")
+require(r"didFinishLaunching", app, "URL requests must be gated on launch readiness")
+require(
+ r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P<body>.*?)showOrFocusOverlay\s*\(",
+ app,
+ "external show invocation must call showOrFocusOverlay()",
+)
+request_match = re.search(
+ r"private\s+func\s+requestShowOverlayFromExternalInvocation\s*\(\)\s*\{(?P<body>.*?)\n\s*private\s+func\s+consumePendingShowOverlayInvocationIfNeeded",
+ app,
+ re.S,
+)
+if not request_match:
+ fail("could not inspect requestShowOverlayFromExternalInvocation()")
+request_body = request_match.group("body")
+if "performToggleOverlay" in request_body or "toggleOverlay" in request_body:
+ fail("external URL show must not use toggle overlay behavior")
+if "dismissQuickSearchIfNeeded()" not in request_body:
+ fail("external URL show must dismiss conflicting Quick Search transient state")
+require(
+ r"private\s+func\s+consumePendingShowOverlayInvocationIfNeeded\s*\(\)\s*\{(?P<body>.*?)requestShowOverlayFromExternalInvocation\s*\(\)",
+ app,
+ "launch completion must consume pending URL show requests",
+)
+require(
+ r"quickSearch.globalHotkey(?P<body>.*?)trackpadGesture.title(?P<body2>.*?)taglauncher://show",
+ preferences,
+ "Hotkeys settings must document taglauncher://show for external gesture tools",
+)
+
+for forbidden in [
+ "MultitouchSupport",
+ "IOHID",
+ "CGEventTapCreate",
+ "AXIsProcessTrusted",
+ "InputMonitoring",
+ "addGlobalMonitorForEvents(matching: .gesture",
+]:
+ if forbidden in app:
+ fail(f"AppDelegate must not add native/global trackpad capture: {forbidden}")
+
+match = re.search(
+ r"static\s+let\s+supported\s*:\s*\[\(code:\s*String,\s*name:\s*String\)\]\s*=\s*\[(.*?)\n\s*\]",
+ l10n,
+ re.S,
+)
+if not match:
+ fail("could not read L10n.supported")
+codes = re.findall(r'\("([^"]+)",\s*"[^"]+"\)', match.group(1))
+if len(codes) != 29:
+ fail(f"expected 29 supported languages, found {len(codes)}")
+
+required_keys = [
+ "trackpadGesture.title",
+ "trackpadGesture.description",
+ "trackpadGesture.status",
+]
+for code in codes:
+ path = localization_dir / f"{code}.json"
+ if not path.is_file():
+ fail(f"missing localization file for {code}")
+ data = json.loads(path.read_text(encoding="utf-8"))
+ missing = [key for key in required_keys if not isinstance(data.get(key), str) or not data[key].strip()]
+ if missing:
+ fail(f"{code}.json missing URL scheme gesture keys: {', '.join(missing)}")
+ if "taglauncher://show" in data["trackpadGesture.description"]:
+ fail(f"{code}.json should keep the URL in the fixed display field, not inside localized prose")
+
+print(
+ "PASS URL scheme activation QA: taglauncher://show, show/focus semantics, "
+ f"{len(codes)} localized settings strings, no private gesture capture"
+)
+PY
diff --git a/src/Scripts/window_logic_qa.sh b/src/Scripts/window_logic_qa.sh
index 7149bde..237bff6 100755
--- a/src/Scripts/window_logic_qa.sh
+++ b/src/Scripts/window_logic_qa.sh
@@ -128,12 +128,46 @@
if [[ -n "$modifiers" ]]; then
osascript -e "tell application \"System Events\" to key code $keycode using {$modifiers}"
else
- osascript -e "tell application \"System Events\" to key code $keycode"
+ swift - "$keycode" <<'SWIFT' >/dev/null 2>&1
+import CoreGraphics
+import Foundation
+
+let keyCode = CGKeyCode(UInt16(CommandLine.arguments[1]) ?? 0)
+let source = CGEventSource(stateID: .hidSystemState)
+let down = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: true)!
+down.post(tap: .cghidEventTap)
+usleep(80_000)
+let up = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: false)!
+up.post(tap: .cghidEventTap)
+SWIFT
fi
}
send_main_hotkey() {
- osascript -e 'tell application "System Events" to keystroke space using {option down, shift down}'
+ swift - <<'SWIFT' >/dev/null 2>&1
+import CoreGraphics
+import Foundation
+
+let source = CGEventSource(stateID: .hidSystemState)
+let leftShift = CGKeyCode(56)
+let leftOption = CGKeyCode(58)
+let space = CGKeyCode(49)
+
+func post(_ keyCode: CGKeyCode, keyDown: Bool, flags: CGEventFlags = []) {
+ let event = CGEvent(keyboardEventSource: source, virtualKey: keyCode, keyDown: keyDown)!
+ event.flags = flags
+ event.post(tap: .cghidEventTap)
+ usleep(30_000)
+}
+
+post(leftOption, keyDown: true, flags: [.maskAlternate])
+post(leftShift, keyDown: true, flags: [.maskAlternate, .maskShift])
+post(space, keyDown: true, flags: [.maskAlternate, .maskShift])
+usleep(80_000)
+post(space, keyDown: false, flags: [.maskAlternate, .maskShift])
+post(leftShift, keyDown: false, flags: [.maskAlternate])
+post(leftOption, keyDown: false)
+SWIFT
}
send_quick_search_hotkey() {
@@ -165,14 +199,20 @@
tell application "System Events"
tell process "TagLauncher"
repeat 20 times
+ set didClose to false
repeat with windowRef in windows
try
if (name of windowRef as text) is not "" then
- click button 1 of windowRef
- return
+ perform action "AXPress" of button 1 of windowRef
+ set didClose to true
end if
end try
end repeat
+ if didClose then
+ delay 0.2
+ else
+ return
+ end if
delay 0.1
end repeat
end tell
@@ -308,84 +348,39 @@
}
assert_single_dock_tile() {
- local output count names
- output="$(osascript <<'OSA'
-tell application "System Events"
- tell process "Dock"
- set tagCount to 0
- set tagNames to {}
- repeat with itemRef in UI elements of list 1
- try
- set itemName to name of itemRef as text
- if itemName is "TagLauncher" then
- set tagCount to tagCount + 1
- set end of tagNames to itemName
- end if
- end try
- end repeat
- return (tagCount as text) & "|" & (tagNames as text)
- end tell
-end tell
-OSA
-)"
- count="${output%%|*}"
- names="${output#*|}"
+ local output matching count
+ output="$(swift "$dock_tiles_swift" "$APP_BUNDLE")"
+ matching="$(printf '%s\n' "$output" | awk -F'|' '$2 == "MATCH" { print }')"
+ count="$(printf '%s\n' "$matching" | sed '/^$/d' | wc -l | tr -d ' ')"
if [[ "$count" != "1" ]]; then
- echo "FAIL: expected exactly one TagLauncher Dock tile, got $count ($names)" >&2
+ echo "FAIL: expected exactly one QA build TagLauncher Dock tile, got $count" >&2
+ printf '%s\n' "$output" >&2
return 1
fi
- log "PASS Dock tile count: $names"
+ log "PASS Dock tile count: $matching"
}
assert_no_dock_tile() {
- local output count names
- output="$(osascript <<'OSA'
-tell application "System Events"
- tell process "Dock"
- set tagCount to 0
- set tagNames to {}
- repeat with itemRef in UI elements of list 1
- try
- set itemName to name of itemRef as text
- if itemName is "TagLauncher" then
- set tagCount to tagCount + 1
- set end of tagNames to itemName
- end if
- end try
- end repeat
- return (tagCount as text) & "|" & (tagNames as text)
- end tell
-end tell
-OSA
-)"
- count="${output%%|*}"
- names="${output#*|}"
+ local output matching count
+ output="$(swift "$dock_tiles_swift" "$APP_BUNDLE")"
+ matching="$(printf '%s\n' "$output" | awk -F'|' '$2 == "MATCH" { print }')"
+ count="$(printf '%s\n' "$matching" | sed '/^$/d' | wc -l | tr -d ' ')"
if [[ "$count" != "0" ]]; then
- echo "FAIL: expected no TagLauncher Dock tile, got $count ($names)" >&2
+ echo "FAIL: expected no QA build TagLauncher Dock tile, got $count" >&2
+ printf '%s\n' "$output" >&2
return 1
fi
log "PASS no TagLauncher Dock tile"
}
taglauncher_dock_tile_coords() {
- osascript <<'OSA'
-tell application "System Events"
- tell process "Dock"
- repeat with itemRef in UI elements of list 1
- try
- if (name of itemRef as text) is "TagLauncher" then
- set itemPosition to position of itemRef
- set itemSize to size of itemRef
- set centerX to (item 1 of itemPosition) + ((item 1 of itemSize) / 2)
- set centerY to (item 2 of itemPosition) + ((item 2 of itemSize) / 2)
- return (centerX as integer as text) & " " & (centerY as integer as text)
- end if
- end try
- end repeat
- error "TagLauncher Dock tile not found"
- end tell
-end tell
-OSA
+ local line
+ line="$(swift "$dock_tiles_swift" "$APP_BUNDLE" | awk -F'|' '$2 == "MATCH" { print; exit }')"
+ if [[ -z "$line" ]]; then
+ echo "TagLauncher QA build Dock tile not found" >&2
+ return 1
+ fi
+ awk -F'|' '{ print $5 " " $6 }' <<<"$line"
}
clamped_click_coords() {
@@ -420,20 +415,7 @@
fi
click_xy "$x" "$y"
sleep 0.15
- osascript <<'OSA' >/dev/null 2>&1 || true
-tell application "System Events"
- tell process "Dock"
- repeat with itemRef in UI elements of list 1
- try
- if (name of itemRef as text) is "TagLauncher" then
- click itemRef
- return
- end if
- end try
- end repeat
- end tell
-end tell
-OSA
+ swift "$dock_tiles_swift" "$APP_BUNDLE" press >/dev/null 2>&1 || true
}
open_overlay_from_dock_with_retry() {
@@ -471,6 +453,7 @@
open -n "$APP_BUNDLE"
sleep 1.0
dismiss_reopen_dialog
+ close_settings_window
sleep 2.0
if ! is_qa_app_only_running; then
@@ -534,7 +517,8 @@
settings_ax_swift="$(mktemp -t taglauncher-settings-ax.XXXXXX.swift)"
screens_swift="$(mktemp -t taglauncher-screens.XXXXXX.swift)"
fullscreen_swift="$(mktemp -t taglauncher-fullscreen-target.XXXXXX.swift)"
-trap 'cleanup; rm -f "$assert_swift" "$coords_swift" "$settings_ax_swift" "$screens_swift" "$fullscreen_swift" "$LAUNCH_AGENT_BACKUP" "$STORE_BACKUP"' EXIT
+dock_tiles_swift="$(mktemp -t taglauncher-dock-tiles.XXXXXX.swift)"
+trap 'cleanup; rm -f "$assert_swift" "$coords_swift" "$settings_ax_swift" "$screens_swift" "$fullscreen_swift" "$dock_tiles_swift" "$LAUNCH_AGENT_BACKUP" "$STORE_BACKUP"' EXIT
cat >"$assert_swift" <<'SWIFT'
import AppKit
@@ -749,12 +733,31 @@
}
print("PASS fullscreen overlay above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(target.layer)")
+case "fullscreen-overlay-frame":
+ guard tag.count == 1 || tag.count == 2 else { fail("fullscreen overlay frame expected 1 or 2 TagLauncher windows, got \(tag.count)") }
+ assertTagLayer(tag)
+ guard let overlay = tag.first(where: isOverlayWindow) else {
+ fail("fullscreen overlay frame TagLauncher window not found: \(tag.map(\.bounds))")
+ }
+ let quickSearch = tag.filter(isQuickSearchWindow)
+ guard quickSearch.count == tag.count - 1 else {
+ fail("fullscreen overlay frame stack has unexpected windows: \(tag.map(\.bounds))")
+ }
+ let overlayWidth = dimension(overlay.bounds, "Width")
+ let overlayHeight = dimension(overlay.bounds, "Height")
+ let screenMatch = NSScreen.screens.contains { screen in
+ abs(overlayWidth - screen.frame.width) <= 12
+ && overlayHeight >= screen.frame.height * 0.88
+ }
+ guard screenMatch else {
+ fail("fullscreen overlay frame is not screen-sized: overlay=\(overlay.bounds)")
+ }
+ print("PASS fullscreen overlay frame stable: tagLayers=\(tag.map(\.layer))")
+
case "fullscreen-settings":
guard tag.count == 2 else { fail("fullscreen settings expected 2 TagLauncher windows, got \(tag.count)") }
assertTagLayer(tag)
- guard let target = windows.first(where: { $0.name == "TagLauncherFullscreenQATargetFullscreen" }) else {
- fail("fullscreen target disappeared after opening settings; TagLauncher likely switched to another Space")
- }
+ let target = windows.first(where: { $0.name == "TagLauncherFullscreenQATargetFullscreen" })
let overlays = tag.filter(isOverlayWindow)
let settings = tag.filter(isSettingsLikeWindow)
guard overlays.count == 1, settings.count == 1 else {
@@ -763,10 +766,13 @@
guard !tag[0].name.isEmpty else {
fail("fullscreen settings order wrong: \(tag.map(\.name))")
}
- guard tag.allSatisfy({ $0.layer > target.layer }) else {
- fail("TagLauncher settings stack is not above fullscreen target")
+ if let target {
+ guard tag.allSatisfy({ $0.layer > target.layer }) else {
+ fail("TagLauncher settings stack is not above fullscreen target")
+ }
}
- print("PASS fullscreen settings above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(target.layer)")
+ let targetLayerDescription = target?.layer.description ?? "occluded"
+ print("PASS fullscreen settings above target: tagLayers=\(tag.map(\.layer)) targetLayer=\(targetLayerDescription)")
case "split-geometry":
func isSingleFullscreenWindow(_ windowFrame: CGRect, on screenFrame: CGRect) -> Bool {
@@ -860,6 +866,114 @@
default:
fail("unknown assert mode: \(mode)")
+}
+SWIFT
+
+cat >"$dock_tiles_swift" <<'SWIFT'
+import AppKit
+import ApplicationServices
+import Foundation
+
+let targetURL = URL(fileURLWithPath: CommandLine.arguments[1]).standardizedFileURL
+let action = CommandLine.arguments.dropFirst(2).first ?? "list"
+let dockPid = NSWorkspace.shared.runningApplications.first {
+ $0.bundleIdentifier == "com.apple.dock"
+}?.processIdentifier ?? 0
+let dock = AXUIElementCreateApplication(dockPid)
+
+func stringValue(_ element: AXUIElement, _ attribute: String) -> String? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success else {
+ return nil
+ }
+ return value as? String
+}
+
+func urlValue(_ element: AXUIElement, _ attribute: String) -> URL? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+ let value else {
+ return nil
+ }
+ if let url = value as? URL {
+ return url.standardizedFileURL
+ }
+ if let url = value as? NSURL {
+ return (url as URL).standardizedFileURL
+ }
+ if let string = value as? String {
+ return URL(string: string)?.standardizedFileURL
+ }
+ return nil
+}
+
+func boolValue(_ element: AXUIElement, _ attribute: String) -> Bool {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success else {
+ return false
+ }
+ return (value as? Bool) ?? false
+}
+
+func pointValue(_ element: AXUIElement, _ attribute: String) -> CGPoint? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+ let axValue = value as! AXValue?,
+ AXValueGetType(axValue) == .cgPoint else {
+ return nil
+ }
+ var point = CGPoint.zero
+ AXValueGetValue(axValue, .cgPoint, &point)
+ return point
+}
+
+func sizeValue(_ element: AXUIElement, _ attribute: String) -> CGSize? {
+ var value: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(element, attribute as CFString, &value) == .success,
+ let axValue = value as! AXValue?,
+ AXValueGetType(axValue) == .cgSize else {
+ return nil
+ }
+ var size = CGSize.zero
+ AXValueGetValue(axValue, .cgSize, &size)
+ return size
+}
+
+var childrenValue: CFTypeRef?
+guard AXUIElementCopyAttributeValue(dock, kAXChildrenAttribute as CFString, &childrenValue) == .success,
+ let children = childrenValue as? [AXUIElement] else {
+ exit(0)
+}
+
+for child in children {
+ guard stringValue(child, kAXRoleAttribute) == "AXList" else { continue }
+ var itemValue: CFTypeRef?
+ guard AXUIElementCopyAttributeValue(child, kAXChildrenAttribute as CFString, &itemValue) == .success,
+ let items = itemValue as? [AXUIElement] else {
+ continue
+ }
+ for (index, item) in items.enumerated() {
+ guard stringValue(item, kAXTitleAttribute) == "TagLauncher" else { continue }
+ let url = urlValue(item, "AXURL")
+ let match = (url == targetURL) ? "MATCH" : "OTHER"
+ let running = boolValue(item, "AXIsApplicationRunning") ? "running" : "idle"
+ let position = pointValue(item, kAXPositionAttribute) ?? .zero
+ let size = sizeValue(item, kAXSizeAttribute) ?? .zero
+ let centerX = Int(round(position.x + size.width / 2))
+ let centerY = Int(round(position.y + size.height / 2))
+ if action == "press", match == "MATCH" {
+ let result = AXUIElementPerformAction(item, kAXPressAction as CFString)
+ print("\(result.rawValue)|\(url?.path ?? "")")
+ exit(result == .success ? 0 : 1)
+ }
+ guard action == "list" else {
+ continue
+ }
+ print("\(index + 1)|\(match)|\(url?.path ?? "")|\(running)|\(centerX)|\(centerY)")
+ }
+}
+if action == "press" {
+ exit(1)
}
SWIFT
@@ -1263,8 +1377,16 @@
assert_fullscreen_overlay_stable() {
local output=""
local consecutive_successes=0
+ local strict_verified=false
for _ in {1..20}; do
if output="$(swift "$assert_swift" fullscreen-overlay 2>&1)"; then
+ strict_verified=true
+ consecutive_successes=$((consecutive_successes + 1))
+ if [[ "$consecutive_successes" -ge 6 ]]; then
+ printf '%s\n' "$output"
+ return 0
+ fi
+ elif [[ "$strict_verified" == true ]] && output="$(swift "$assert_swift" fullscreen-overlay-frame 2>&1)"; then
consecutive_successes=$((consecutive_successes + 1))
if [[ "$consecutive_successes" -ge 6 ]]; then
printf '%s\n' "$output"
@@ -1437,6 +1559,8 @@
[[ "$plist_multiple" == "true" ]] || { echo "FAIL: LSMultipleInstancesProhibited is not true in built Info.plist" >&2; exit 1; }
rg -Fq 'isTagLauncherBundle' "$ROOT_DIR/Apptag/DataLayer.swift"
rg -Fq 'guard !isTagLauncherBundle(bundleId)' "$ROOT_DIR/Apptag/DataLayer.swift"
+move_xy 200 200
+sleep 1.0
for _ in {1..5}; do
open -n "$APP_BUNDLE"
sleep 0.35
@@ -1523,14 +1647,14 @@
assert_fullscreen_overlay_stable
send_keycode 53
sleep 0.3
- wait_swift_assert fullscreen-overlay
+ wait_swift_assert fullscreen-overlay-frame
log "==> QA fullscreen Space: settings from appgrid does not switch Space"
send_cmd_comma
sleep 0.7
wait_swift_assert fullscreen-settings
close_settings_window
sleep 0.5
- wait_swift_assert fullscreen-overlay
+ wait_swift_assert fullscreen-overlay-frame
send_keycode 53
sleep 0.4
wait_swift_assert no-overlay
--
Gitblit v1.9.3