From 26e2f4de52c4aef06d1b834fc101d7dfac3b810c Mon Sep 17 00:00:00 2001
From: Ariver <ar@MacBook-Air.local>
Date: Sat, 16 May 2026 02:05:32 +0800
Subject: [PATCH] Release TagLauncher 5.7.8 build 583
---
Apptag/ApptagApp.swift | 688 ++++++++++++++++++++++++-------
Apptag/Localization/cs.json | 2
Apptag/Localization/no.json | 2
Apptag/Localization/zh-Hant.json | 2
Apptag/Localization/ja.json | 2
Apptag/Info.plist | 4
Apptag/Localization/th.json | 2
prd-importantback.md | 85 +++
Apptag/Localization/nl.json | 2
Apptag/Localization/sr-Cyrl.json | 2
Apptag/Localization/zh-Hans.json | 2
Apptag/Localization/de.json | 2
Apptag/Localization/ms.json | 2
Apptag/Localization/nn.json | 2
Apptag/Localization/pt-BR.json | 2
Apptag/Localization/es.json | 2
AppStore_TODO_5.7.8_583.md | 55 ++
Apptag/Localization/tr.json | 2
AppStore_Submission.md | 23
Apptag/Localization/en.json | 2
Apptag/Localization/nb.json | 2
Apptag/Localization/ar.json | 2
Apptag/Localization/vi.json | 2
Apptag/Localization/fr.json | 2
Apptag/Localization/id.json | 2
Apptag/Localization/da.json | 2
CHANGELOG.md | 86 +++
Apptag/Localization/uk.json | 2
Apptag/Localization/ar-Najdi.json | 2
Apptag/Localization/ko.json | 2
Apptag/ContentView.swift | 40 -
Apptag/Localization/it.json | 2
Apptag/Localization/sv.json | 2
Apptag/Localization/ro.json | 2
plan-deepthink.md | 262 +++++++++++
Apptag/Localization/pl.json | 2
Apptag/Localization/ru.json | 2
37 files changed, 1,064 insertions(+), 237 deletions(-)
diff --git a/AppStore_Submission.md b/AppStore_Submission.md
index ff1bcc6..8681c7b 100644
--- a/AppStore_Submission.md
+++ b/AppStore_Submission.md
@@ -1,6 +1,6 @@
# Apptag — Mac App Store 提审资料
-> 版本: 5.7.5 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-14
+> 版本: 5.7.8 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-15
---
@@ -11,8 +11,8 @@
| **App 名称** | Apptag |
| **副标题** | Tag-Based App Launcher |
| **Bundle ID** | com.apptag.launcher |
-| **版本号** | 5.7.5 |
-| **Build 号** | 575 |
+| **版本号** | 5.7.8 |
+| **Build 号** | 583 |
| **SKU** | apptag-mac-001 |
| **主要类别** | Utilities (工具) |
| **次要类别** | Productivity (效率) |
@@ -187,9 +187,20 @@
## 8. What's New (本次版本更新说明)
```
-Apptag 5.7.5
+Apptag 5.7.8
-- Fixed the menu bar icon visibility issue and added a Show Menu Bar Icon preference
+- Fixed duplicate overlay keyboard event monitors that could accumulate after repeatedly opening TagLauncher
+- Removed obsolete tag reorder fallback handlers that could interfere with click handling after drag sorting
+- Reduced unnecessary Dock refreshes when unrelated preferences change
+- Made the menu bar icon mandatory and removed the Show Menu Bar Icon preference across all localizations
+- Removed unused macOS app-menu items and added a localized Show App List command with symbol-only shortcut glyphs
+- Replaced the default macOS Services/Hide menu groups and made the menu bar status item use a fixed visible icon slot
+- Replaced the menu bar SF Symbol with a code-drawn template icon so the status item remains visible even if symbol rendering fails
+- Improved the General preferences layout for long localized text by using two-column style choices and aligned setting rows
+- Refined the app-list style chooser so Flat spans the first row and the remaining four styles form a balanced 2x2 grid
+- Reduced the app-list style option height and added compact visual glyphs for flat, masonry, and grid layouts
+- Added a symbol-only shortcut hint on the Language, Data, and About preference tabs for Option + Shift + Space
+- Fixed the menu bar icon visibility issue
- Fixed Preferences placement when opened above TagLauncher from a fullscreen app Space
- Fixed tag navigation reorder mode so clicking elsewhere exits the shaking state
- Expanded localization support to 29 language variants
@@ -205,7 +216,7 @@
- Added Mac App Store sandbox entitlements and App Store build validation
- Hid Launch at Login controls in sandboxed App Store builds
- Removed deprecated app launch API usage
-- Version updated to 5.7.5 (Build 575)
+- Version kept at 5.7.8, Build updated to 583
```
---
diff --git a/AppStore_TODO_5.7.8_583.md b/AppStore_TODO_5.7.8_583.md
new file mode 100644
index 0000000..1177bf2
--- /dev/null
+++ b/AppStore_TODO_5.7.8_583.md
@@ -0,0 +1,55 @@
+# TagLauncher 5.7.8 (Build 583) App Store 提交 To-do List
+
+
+
+> 这个本机 .app/.dmg 是本地构建产物。去 App Store 提交时,最好在那台登录开发者账号的电脑上,用 source/ 里的源码重新用正式证书构建、签名、上传。
+
+## A. 换电脑前确认
+
+- 确认要提交的版本号是 `5.7.8`,Build 是 `583`。
+- 带走本文件夹里的 `AppStore_Submission.md`、`CHANGELOG.md`、`TagLauncher.dmg`、`TagLauncher.app`。
+- 如果要在另一台电脑重新签名构建,也带走源码快照文件夹或整个项目目录。
+- 测试时先退出旧版 TagLauncher,再启动新版,避免菜单栏状态项仍来自旧进程。
+- 如果菜单栏图标看不到,先检查 Thaw / Bartender / Ice / Hidden Bar / Dozer 等菜单栏管理软件是否隐藏了 TagLauncher。
+
+## B. 开发者电脑准备
+
+- 登录正确的 Apple Developer 账号。
+- 确认 Xcode 已安装,并安装 Command Line Tools。
+- 在 Xcode 账号里确认有 Mac App Distribution / Apple Distribution 证书。
+- 确认 App Store Connect 里 Bundle ID 是 `com.apptag.launcher`。
+- 确认 App Store Connect 已创建或选择 Apptag 的 `5.7.8` 版本。
+
+## C. 正式构建与签名
+
+- 在开发者电脑上用开发者证书重新构建,不要直接提交本机 ad-hoc 签名包。
+- 使用 App Store sandbox entitlements:`Apptag/TagLauncher.entitlements`。
+- 构建完成后确认 About 页显示 `版本 5.7.8(构建 583)`。
+- 确认 `LSMinimumSystemVersion` 是 `15.0`。
+- 本地打开新构建的 App,快速检查:
+ - `⌥⇧空格` 能呼出应用列表。
+ - 设置窗口能在当前 TagLauncher 浮层上方居中显示。
+ - 设置页语言切换正常。
+ - 通用页排版在英文、法语、意大利语、中文下都不拥挤。
+ - 数据页导入/导出分类与布局正常。
+
+## D. App Store Connect 填写
+
+- 复制 `AppStore_Submission.md` 里的 App 描述、关键词、隐私说明和审核备注。
+- 隐私标签选择“不收集任何数据”。
+- 最低系统版本填写 macOS `15.0`。
+- What's New 使用 `AppStore_Submission.md` 第 8 节。
+- 如果上传截图,优先展示:
+ - 主应用列表视图。
+ - 设置页语言切换。
+ - 通用页布局设置。
+ - 标签编辑/拖动排序。
+ - 导入/导出分类与布局。
+
+## E. 上传与提交审核
+
+- 用 Xcode Organizer / Transporter 上传正式签名构建。
+- 上传后在 App Store Connect 选择 Build `583`。
+- 填完审核信息后提交审核。
+- 提交后保留本提交包,方便审核反馈时对照版本。
+
diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index db83bfe..a04f8b8 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -13,6 +13,10 @@
PreferencesView()
}
.defaultSize(width: 880, height: 460)
+ .commands {
+ CommandGroup(replacing: .systemServices) { }
+ CommandGroup(replacing: .appVisibility) { }
+ }
}
}
@@ -24,29 +28,37 @@
}
final class AppDelegate: NSObject, NSApplicationDelegate {
- private static let showMenuBarIconKey = "showMenuBarIcon"
+ private static let showDockIconKey = "showDockIcon"
+ private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
+ private static let showAppListShortcutGlyphs = "⌥⇧␣"
private var statusItem: NSStatusItem?
private var overlayWindow: NSWindow?
+ private var overlayKeyMonitor: Any?
private var settingsWindow: NSWindow? // Track Settings window to keep it above overlay
private var hotkeyRef: EventHotKeyRef?
private var isInEditMode = false // Suppress auto-dismiss during editing
+ private var isConfiguringApplicationMenu = false
+ private var lastShowDockIcon: Bool?
+
+ static func refreshChromeSettings() {
+ (NSApp.delegate as? AppDelegate)?.syncChromeSettings(force: true)
+ }
func applicationDidFinishLaunching(_ notification: Notification) {
L10n.setup()
- setupDefaultPreferences()
migrateDefaultGroupName()
TagDatabase.seedDefaultTags()
- let showDock = UserDefaults.standard.bool(forKey: "showDockIcon")
- NSApp.setActivationPolicy(showDock ? .regular : .accessory)
- setupMenuBar()
+ syncChromeSettings(force: true)
registerHotkey()
observeOtherWindows()
observeSettingsClose()
observeEditMode()
+ observeApplicationMenuChanges()
observeChromeSettings()
observeLanguageChanges()
setupLaunchAtLogin()
+ configureApplicationMenuWhenAvailable()
}
/// Dock icon click → show overlay (same as menubar "Show TagLauncher")
@@ -73,21 +85,27 @@
// User has set a custom name — keep it
}
- private func setupDefaultPreferences() {
- if UserDefaults.standard.object(forKey: Self.showMenuBarIconKey) == nil {
- UserDefaults.standard.set(true, forKey: Self.showMenuBarIconKey)
- }
- }
-
- /// Observe Dock/menu-bar visibility changes so they take effect immediately.
+ /// Observe Dock visibility changes so they take effect immediately.
private func observeChromeSettings() {
NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: nil, queue: .main
) { [weak self] _ in
- let show = UserDefaults.standard.bool(forKey: "showDockIcon")
- NSApp.setActivationPolicy(show ? .regular : .accessory)
- self?.setupMenuBar()
+ self?.syncChromeSettings()
+ }
+ }
+
+ private func syncChromeSettings(force: Bool = false) {
+ let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
+ let dockChanged = lastShowDockIcon != showDock
+
+ if force || dockChanged {
+ NSApp.setActivationPolicy(showDock ? .regular : .accessory)
+ lastShowDockIcon = showDock
+ }
+
+ if force {
+ setupMenuBar()
}
}
@@ -98,6 +116,7 @@
object: nil, queue: .main
) { [weak self] _ in
self?.setupMenuBar()
+ self?.configureApplicationMenuWhenAvailable()
}
}
@@ -160,35 +179,28 @@
// MARK: - Menu Bar
private func setupMenuBar() {
- guard UserDefaults.standard.object(forKey: Self.showMenuBarIconKey) as? Bool ?? true else {
- removeMenuBarItem()
- return
- }
-
- if statusItem == nil {
- statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
- }
+ removeMenuBarItem()
+ statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
guard let statusItem else { return }
+ statusItem.isVisible = true
if let button = statusItem.button {
- let image = makeMenuBarIcon()
- button.image = image
+ button.image = makeMenuBarIcon()
button.imageScaling = .scaleProportionallyDown
button.imagePosition = .imageOnly
- button.title = image == nil ? "T" : ""
button.toolTip = "TagLauncher — Tag-based app launcher"
button.action = #selector(toggleOverlay)
button.target = self
}
let menu = NSMenu()
- menu.addItem(
- NSMenuItem(
- title: "\(tr("menu.show")) ⇧⌥Space",
- action: #selector(toggleOverlay),
- keyEquivalent: ""
- )
+ let showItem = NSMenuItem(
+ title: showAppListMenuTitle,
+ action: #selector(toggleOverlay),
+ keyEquivalent: ""
)
+ showItem.target = self
+ menu.addItem(showItem)
menu.addItem(.separator())
// Version display
@@ -236,31 +248,166 @@
statusItem.menu = menu
}
+ private func makeMenuBarIcon() -> NSImage {
+ let image = NSImage(size: NSSize(width: 18, height: 18))
+ image.lockFocus()
+ defer { image.unlockFocus() }
+
+ NSColor.black.setFill()
+
+ let tagBody = NSBezierPath()
+ tagBody.move(to: NSPoint(x: 5.0, y: 2.2))
+ tagBody.line(to: NSPoint(x: 15.6, y: 5.0))
+ tagBody.line(to: NSPoint(x: 12.8, y: 15.8))
+ tagBody.line(to: NSPoint(x: 2.2, y: 13.0))
+ tagBody.close()
+ tagBody.fill()
+
+ NSGraphicsContext.current?.compositingOperation = .clear
+ NSBezierPath(ovalIn: NSRect(x: 10.9, y: 11.2, width: 3.2, height: 3.2)).fill()
+ NSGraphicsContext.current?.compositingOperation = .sourceOver
+
+ image.isTemplate = true
+ image.accessibilityDescription = "TagLauncher"
+ return image
+ }
+
+ private var showAppListMenuTitle: String {
+ "\(tr("menu.showAppList")) \(Self.showAppListShortcutGlyphs)"
+ }
+
+ private func observeApplicationMenuChanges() {
+ NotificationCenter.default.addObserver(
+ forName: NSApplication.didBecomeActiveNotification,
+ object: NSApp, queue: .main
+ ) { [weak self] _ in
+ self?.configureApplicationMenuWhenAvailable(retries: 4)
+ }
+
+ NotificationCenter.default.addObserver(
+ forName: NSMenu.didAddItemNotification,
+ object: nil, queue: .main
+ ) { [weak self] notification in
+ guard let self,
+ !self.isConfiguringApplicationMenu,
+ let menu = notification.object as? NSMenu,
+ menu === NSApp.mainMenu?.items.first?.submenu
+ else { return }
+ self.configureApplicationMenuWhenAvailable(retries: 2)
+ }
+ }
+
+ private func configureApplicationMenuWhenAvailable(retries: Int = 20) {
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
+ guard let self else { return }
+ guard NSApp.mainMenu?.items.first?.submenu != nil else {
+ if retries > 0 {
+ self.configureApplicationMenuWhenAvailable(retries: retries - 1)
+ }
+ return
+ }
+
+ self.configureApplicationMenu()
+ if retries > 0 && self.applicationMenuNeedsCleanup() {
+ self.configureApplicationMenuWhenAvailable(retries: retries - 1)
+ }
+ }
+ }
+
+ private func configureApplicationMenu() {
+ guard !isConfiguringApplicationMenu else { return }
+ guard let appMenu = NSApp.mainMenu?.items.first?.submenu else { return }
+
+ isConfiguringApplicationMenu = true
+ defer { isConfiguringApplicationMenu = false }
+
+ removeUnusedDefaultItems(from: appMenu)
+ upsertShowAppListItem(in: appMenu)
+ normalizeSeparators(in: appMenu)
+ }
+
+ private func applicationMenuNeedsCleanup() -> Bool {
+ guard let appMenu = NSApp.mainMenu?.items.first?.submenu else { return true }
+ let hasUnusedDefaultItems = appMenu.items.contains(where: isUnusedDefaultApplicationMenuItem)
+ let hasShowAppListItem = appMenu.items.contains {
+ $0.identifier == Self.showAppListMenuItemIdentifier
+ }
+ return hasUnusedDefaultItems || !hasShowAppListItem
+ }
+
+ private func removeUnusedDefaultItems(from menu: NSMenu) {
+ for item in menu.items.reversed() where isUnusedDefaultApplicationMenuItem(item) {
+ menu.removeItem(item)
+ }
+ }
+
+ private func isUnusedDefaultApplicationMenuItem(_ item: NSMenuItem) -> Bool {
+ if let servicesMenu = NSApp.servicesMenu, item.submenu === servicesMenu {
+ return true
+ }
+ return item.action == #selector(NSApplication.hide(_:))
+ || item.action == #selector(NSApplication.hideOtherApplications(_:))
+ || item.action == #selector(NSApplication.unhideAllApplications(_:))
+ }
+
+ private func upsertShowAppListItem(in menu: NSMenu) {
+ if let existingItem = menu.items.first(where: { $0.identifier == Self.showAppListMenuItemIdentifier }) {
+ configureShowAppListItem(existingItem)
+ return
+ }
+
+ let item = NSMenuItem()
+ item.identifier = Self.showAppListMenuItemIdentifier
+ configureShowAppListItem(item)
+
+ if let settingsIndex = menu.items.firstIndex(where: { isSettingsMenuItem($0) }) {
+ menu.insertItem(item, at: settingsIndex + 1)
+ } else if let firstSeparatorIndex = menu.items.firstIndex(where: { $0.isSeparatorItem }) {
+ menu.insertItem(item, at: firstSeparatorIndex)
+ } else {
+ menu.addItem(item)
+ }
+ }
+
+ private func configureShowAppListItem(_ item: NSMenuItem) {
+ item.title = showAppListMenuTitle
+ item.action = #selector(toggleOverlay)
+ item.target = self
+ item.keyEquivalent = ""
+ item.keyEquivalentModifierMask = []
+ item.isEnabled = true
+ }
+
+ private func isSettingsMenuItem(_ item: NSMenuItem) -> Bool {
+ item.action == Selector(("showSettingsWindow:"))
+ || item.action == #selector(openPreferences)
+ || item.title == tr("menu.preferences")
+ }
+
+ private func normalizeSeparators(in menu: NSMenu) {
+ var indexesToRemove: [Int] = []
+ var previousWasSeparator = false
+
+ for (index, item) in menu.items.enumerated() {
+ guard item.isSeparatorItem else {
+ previousWasSeparator = false
+ continue
+ }
+ if index == 0 || index == menu.items.count - 1 || previousWasSeparator {
+ indexesToRemove.append(index)
+ }
+ previousWasSeparator = true
+ }
+
+ for index in indexesToRemove.reversed() {
+ menu.removeItem(at: index)
+ }
+ }
+
private func removeMenuBarItem() {
guard let statusItem else { return }
NSStatusBar.system.removeStatusItem(statusItem)
self.statusItem = nil
- }
-
- private func makeMenuBarIcon() -> NSImage? {
- let size = NSSize(width: 18, height: 18)
- let image = NSImage(size: size)
- image.lockFocus()
- NSColor.black.setFill()
- let tagPath = NSBezierPath()
- tagPath.move(to: NSPoint(x: 2.5, y: 10.5))
- tagPath.line(to: NSPoint(x: 8.5, y: 16.5))
- tagPath.line(to: NSPoint(x: 15.5, y: 16.5))
- tagPath.line(to: NSPoint(x: 15.5, y: 9.5))
- tagPath.line(to: NSPoint(x: 8.5, y: 2.5))
- tagPath.close()
- tagPath.appendOval(in: NSRect(x: 10.6, y: 12.2, width: 2.6, height: 2.6))
- tagPath.windingRule = .evenOdd
- tagPath.fill()
- image.unlockFocus()
- image.isTemplate = true
- image.accessibilityDescription = "TagLauncher"
- return image
}
// MARK: - Overlay Window
@@ -285,17 +432,28 @@
overlayWindow?.setFrame(screen.frame, display: true)
overlayWindow?.level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.maximumWindow)))
- // Local key monitor: catch Escape while overlay is up
- NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
+ installOverlayKeyMonitor()
+
+ overlayWindow?.makeKeyAndOrderFront(nil)
+ overlayWindow?.orderFrontRegardless()
+ }
+
+ private func installOverlayKeyMonitor() {
+ guard overlayKeyMonitor == nil else { return }
+ overlayKeyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event in
if event.keyCode == 53 { // Escape
self?.hideOverlay()
return nil
}
return event
}
+ }
- overlayWindow?.makeKeyAndOrderFront(nil)
- overlayWindow?.orderFrontRegardless()
+ private func removeOverlayKeyMonitor() {
+ if let monitor = overlayKeyMonitor {
+ NSEvent.removeMonitor(monitor)
+ overlayKeyMonitor = nil
+ }
}
private func makeOverlayWindow(on screen: NSScreen) -> NSWindow {
@@ -333,7 +491,11 @@
private func hideOverlay(force: Bool = false) {
guard force || !isInEditMode else { return }
+ if let settingsWindow, settingsWindow.parent == overlayWindow {
+ detachSettingsWindow(settingsWindow)
+ }
overlayWindow?.orderOut(nil)
+ removeOverlayKeyMonitor()
if force {
overlayWindow = nil
}
@@ -419,8 +581,8 @@
/// Settings must always appear centered over the current overlay view and float above it.
private func prepareSettingsWindow(_ window: NSWindow) {
- window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
let settingsSize = NSSize(width: 880, height: 460)
+ window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
window.minSize = settingsSize
window.maxSize = settingsSize
if abs(window.frame.width - settingsSize.width) > 0.5 || abs(window.frame.height - settingsSize.height) > 0.5 {
@@ -432,15 +594,18 @@
if let overlayWindow, overlayWindow.isVisible {
center(window, over: overlayWindow.frame)
- window.level = NSWindow.Level(rawValue: overlayWindow.level.rawValue + 1)
attachSettingsWindow(window, to: overlayWindow)
+ window.level = overlayWindow.level
} else if let screen = screenUnderMouse() {
center(window, over: screen.visibleFrame)
window.level = .floating
detachSettingsWindow(window)
}
- window.collectionBehavior.formUnion([.canJoinAllSpaces, .fullScreenAuxiliary, .moveToActiveSpace])
+ var behavior = window.collectionBehavior
+ behavior.remove(.canJoinAllSpaces)
+ behavior.formUnion([.fullScreenAuxiliary, .moveToActiveSpace])
+ window.collectionBehavior = behavior
window.makeKeyAndOrderFront(nil)
window.orderFrontRegardless()
settingsWindow = window
@@ -618,8 +783,9 @@
struct PreferencesView: View {
private let settingsWindowWidth: CGFloat = 880
private let settingsContentWidth: CGFloat = 820
- private let settingsLabelWidth: CGFloat = 160
- private let widePickerWidth: CGFloat = 650
+ private let generalContentWidth: CGFloat = 720
+ private let generalLabelWidth: CGFloat = 190
+ private let generalControlWidth: CGFloat = 500
private let compactPickerWidth: CGFloat = 320
@AppStorage("tagFontSize") private var tagFontSize: Double = 18
@@ -628,7 +794,6 @@
@AppStorage("defaultGroupName") private var defaultGroupName = "Other"
@AppStorage("displayMode") private var displayMode = "flat"
@AppStorage("hideAppNames") private var hideAppNames = false
- @AppStorage("showMenuBarIcon") private var showMenuBarIcon = true
@AppStorage("showDockIcon") private var showDockIcon = false
@AppStorage("launchAtLogin") private var launchAtLogin = true
@State private var selectedLanguage = L10n.currentCode
@@ -702,6 +867,52 @@
GridItem(.flexible(minimum: 220), spacing: 18, alignment: .top),
]
}
+ private var displayModeColumns: [GridItem] {
+ [
+ GridItem(.flexible(minimum: 220), spacing: 10, alignment: .top),
+ GridItem(.flexible(minimum: 220), spacing: 10, alignment: .top),
+ ]
+ }
+ private var displayModeOptions: [(id: String, title: String)] {
+ [
+ ("flat", tr("settings.flat")),
+ ("container", tr("settings.container")),
+ ("coloredContainer", tr("settings.coloredContainer")),
+ ("gridContainer", tr("settings.gridContainer")),
+ ("coloredGridContainer", tr("settings.coloredGridContainer")),
+ ]
+ }
+ private var containerDisplayModeOptions: [(id: String, title: String)] {
+ Array(displayModeOptions.dropFirst())
+ }
+
+ private func generalSettingRow<Control: View>(
+ _ label: String,
+ description: String,
+ @ViewBuilder control: () -> Control
+ ) -> some View {
+ HStack(alignment: .top, spacing: 16) {
+ Text(label)
+ .font(.system(size: 14, weight: .semibold))
+ .multilineTextAlignment(.trailing)
+ .lineLimit(2)
+ .minimumScaleFactor(0.86)
+ .frame(width: generalLabelWidth, alignment: .trailing)
+ .padding(.top, 5)
+
+ VStack(alignment: .leading, spacing: 6) {
+ control()
+ .frame(width: generalControlWidth, alignment: .leading)
+ Text(description)
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.leading)
+ .fixedSize(horizontal: false, vertical: true)
+ .frame(width: generalControlWidth, alignment: .leading)
+ }
+ }
+ .frame(width: generalContentWidth, alignment: .leading)
+ }
var body: some View {
ZStack {
@@ -741,6 +952,12 @@
}
.padding(.vertical, 4)
}
+ .frame(maxHeight: 250)
+
+ Spacer(minLength: 0)
+ ShortcutHintView()
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.bottom, 2)
}
.frame(maxWidth: settingsContentWidth, alignment: .leading)
.tabItem { Label(tr("settings.language"), systemImage: "globe") }
@@ -760,8 +977,10 @@
}
}
}
- Toggle(tr("settings.showMenuBarIcon"), isOn: $showMenuBarIcon)
Toggle(tr("settings.showInDock"), isOn: $showDockIcon)
+ .onChange(of: showDockIcon) { _, _ in
+ AppDelegate.refreshChromeSettings()
+ }
Toggle(tr("settings.hideAppNames"), isOn: $hideAppNames)
}
.frame(maxWidth: .infinity, alignment: .center)
@@ -770,78 +989,64 @@
Divider()
.padding(.bottom, 16)
- // Two-column layout: each row label (right-aligned) + controls (left-aligned)
- // All pickers and descriptions share the same left edge
- VStack(spacing: 16) {
- HStack(alignment: .top, spacing: 16) {
- Text(tr("settings.appListStyle"))
- .frame(width: settingsLabelWidth, alignment: .trailing)
- VStack(alignment: .leading, spacing: 4) {
- Picker("", selection: $displayMode) {
- Text(tr("settings.flat")).tag("flat")
- Text(tr("settings.container")).tag("container")
- Text(tr("settings.coloredContainer")).tag("coloredContainer")
- Text(tr("settings.gridContainer")).tag("gridContainer")
- Text(tr("settings.coloredGridContainer")).tag("coloredGridContainer")
+ VStack(spacing: 18) {
+ generalSettingRow(tr("settings.appListStyle"), description: tr("settings.flatDesc")) {
+ VStack(spacing: 8) {
+ DisplayModeOptionButton(
+ mode: "flat",
+ title: tr("settings.flat"),
+ isSelected: displayMode == "flat"
+ ) {
+ displayMode = "flat"
}
- .pickerStyle(.segmented)
- .frame(width: widePickerWidth, alignment: .leading)
- Text(tr("settings.flatDesc"))
- .font(.caption).foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- HStack(alignment: .top, spacing: 16) {
- Text(tr("settings.tagPosition"))
- .frame(width: settingsLabelWidth, alignment: .trailing)
- VStack(alignment: .leading, spacing: 4) {
- Picker("", selection: $tagPosition) {
- Text(tr("settings.left")).tag("left")
- Text(tr("settings.right")).tag("right")
- Text(tr("settings.top")).tag("top")
- }
- .pickerStyle(.segmented)
- .frame(width: compactPickerWidth, alignment: .leading)
- Text(tr("settings.tagPosDesc"))
- .font(.caption).foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
- }
- }
- HStack(alignment: .top, spacing: 16) {
- Text(tr("settings.tagFontSize"))
- .frame(width: settingsLabelWidth, alignment: .trailing)
- VStack(alignment: .leading, spacing: 4) {
- Picker("", selection: $tagFontSize) {
- ForEach([16.0, 18.0, 20.0, 22.0, 24.0, 26.0], id: \.self) { size in
- Text("\(Int(size))").tag(size)
+
+ LazyVGrid(columns: displayModeColumns, alignment: .leading, spacing: 8) {
+ ForEach(containerDisplayModeOptions, id: \.id) { option in
+ DisplayModeOptionButton(
+ mode: option.id,
+ title: option.title,
+ isSelected: displayMode == option.id
+ ) {
+ displayMode = option.id
+ }
}
}
- .pickerStyle(.segmented)
- .frame(width: compactPickerWidth, alignment: .leading)
- Text(tr("settings.tagFontDesc"))
- .font(.caption).foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
}
+ .frame(width: generalControlWidth, alignment: .leading)
}
- HStack(alignment: .top, spacing: 16) {
- Text(tr("settings.iconSize"))
- .frame(width: settingsLabelWidth, alignment: .trailing)
- VStack(alignment: .leading, spacing: 4) {
- Picker("", selection: $iconSize) {
- ForEach([40.0, 48.0, 56.0, 64.0, 72.0, 80.0], id: \.self) { size in
- Text("\(Int(size))").tag(size)
- }
- }
- .pickerStyle(.segmented)
- .frame(width: compactPickerWidth, alignment: .leading)
- Text(tr("settings.iconSizeDesc"))
- .font(.caption).foregroundStyle(.secondary)
- .fixedSize(horizontal: false, vertical: true)
+
+ generalSettingRow(tr("settings.tagPosition"), description: tr("settings.tagPosDesc")) {
+ Picker("", selection: $tagPosition) {
+ Text(tr("settings.left")).tag("left")
+ Text(tr("settings.right")).tag("right")
+ Text(tr("settings.top")).tag("top")
}
+ .pickerStyle(.segmented)
+ .frame(width: compactPickerWidth, alignment: .leading)
+ }
+
+ generalSettingRow(tr("settings.tagFontSize"), description: tr("settings.tagFontDesc")) {
+ Picker("", selection: $tagFontSize) {
+ ForEach([16.0, 18.0, 20.0, 22.0, 24.0, 26.0], id: \.self) { size in
+ Text("\(Int(size))").tag(size)
+ }
+ }
+ .pickerStyle(.segmented)
+ .frame(width: compactPickerWidth, alignment: .leading)
+ }
+
+ generalSettingRow(tr("settings.iconSize"), description: tr("settings.iconSizeDesc")) {
+ Picker("", selection: $iconSize) {
+ ForEach([40.0, 48.0, 56.0, 64.0, 72.0, 80.0], id: \.self) { size in
+ Text("\(Int(size))").tag(size)
+ }
+ }
+ .pickerStyle(.segmented)
+ .frame(width: compactPickerWidth, alignment: .leading)
}
}
}
- .frame(maxWidth: settingsContentWidth, alignment: .center)
+ .frame(maxWidth: generalContentWidth, alignment: .center)
.padding()
.tabItem { Label(tr("settings.general"), systemImage: "gearshape") }
@@ -858,8 +1063,12 @@
.onAppear { scanApps() }
// Tab 4: Data
- Form {
- Section {
+ VStack(spacing: 0) {
+ Spacer(minLength: 96)
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text(tr("settings.backup"))
+ .font(.headline)
HStack(spacing: 12) {
Button(tr("settings.export")) { exportTags() }
.buttonStyle(.bordered)
@@ -869,46 +1078,55 @@
Text(tr("settings.backupDesc"))
.font(.caption)
.foregroundStyle(.secondary)
- } header: {
- Text(tr("settings.backup"))
}
+ .frame(width: 420, alignment: .leading)
+
+ Spacer(minLength: 42)
+ ShortcutHintView()
+ .frame(maxWidth: .infinity, alignment: .center)
+ Spacer(minLength: 18)
}
.tabItem { Label(tr("settings.data"), systemImage: "externaldrive.fill") }
.padding()
// Tab 5: About
- Form {
- Section {
- HStack {
- if let icon = NSImage(named: NSImage.applicationIconName) {
- Image(nsImage: icon)
- .resizable()
- .frame(width: 128, height: 128)
- }
- VStack(alignment: .leading, spacing: 4) {
- Text("TagLauncher")
- .font(.title2)
- .fontWeight(.semibold)
- Text(tr("app.description"))
- .font(.body)
- .foregroundStyle(.secondary)
- Text("\(tr("app.version")) \(appVersion) (\(tr("app.build")) \(buildVersion))")
- .font(.callout)
- .foregroundStyle(.tertiary)
+ VStack(spacing: 0) {
+ Spacer(minLength: 72)
- Divider()
- .padding(.vertical, 4)
-
- Text("万物之中,希望最美")
- .font(.caption)
- .foregroundStyle(.secondary)
- Text("永桔@2026-18602102518")
- .font(.caption)
- .foregroundStyle(.secondary)
- }
+ HStack {
+ if let icon = NSImage(named: NSImage.applicationIconName) {
+ Image(nsImage: icon)
+ .resizable()
+ .frame(width: 128, height: 128)
}
- .padding(.leading, 100)
+ VStack(alignment: .leading, spacing: 4) {
+ Text("TagLauncher")
+ .font(.title2)
+ .fontWeight(.semibold)
+ Text(tr("app.description"))
+ .font(.body)
+ .foregroundStyle(.secondary)
+ Text("\(tr("app.version")) \(appVersion) (\(tr("app.build")) \(buildVersion))")
+ .font(.callout)
+ .foregroundStyle(.tertiary)
+
+ Divider()
+ .padding(.vertical, 4)
+
+ Text("万物之中,希望最美")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ Text("永桔@2026-18602102518")
+ .font(.caption)
+ .foregroundStyle(.secondary)
+ }
}
+ .frame(width: 560, alignment: .leading)
+
+ Spacer(minLength: 30)
+ ShortcutHintView()
+ .frame(maxWidth: .infinity, alignment: .center)
+ Spacer(minLength: 18)
}
.tabItem { Label(tr("settings.about"), systemImage: "info.circle") }
.padding()
@@ -947,3 +1165,153 @@
}
}
}
+
+private struct DisplayModeOptionButton: View {
+ let mode: String
+ let title: String
+ let isSelected: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 7) {
+ DisplayModeGlyph(mode: mode, isSelected: isSelected)
+ .frame(width: 24, height: 18)
+ Text(title)
+ .font(.system(size: 13, weight: .semibold))
+ .multilineTextAlignment(.center)
+ .lineLimit(2)
+ .minimumScaleFactor(0.82)
+ .foregroundStyle(isSelected ? Color.white : Color.primary)
+ .frame(maxWidth: .infinity)
+ }
+ .frame(maxWidth: .infinity, minHeight: 34)
+ .padding(.horizontal, 9)
+ .padding(.vertical, 2)
+ .background(
+ RoundedRectangle(cornerRadius: 7, style: .continuous)
+ .fill(isSelected ? Color.accentColor : Color(nsColor: .controlBackgroundColor))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 7, style: .continuous)
+ .stroke(isSelected ? Color.accentColor : Color.secondary.opacity(0.18), lineWidth: 1)
+ )
+ .contentShape(RoundedRectangle(cornerRadius: 7, style: .continuous))
+ }
+ .buttonStyle(.plain)
+ .accessibilityLabel(title)
+ }
+}
+
+private struct DisplayModeGlyph: View {
+ let mode: String
+ let isSelected: Bool
+
+ var body: some View {
+ switch mode {
+ case "flat":
+ HStack(spacing: 2) {
+ cell(index: 0, width: 5, height: 5, colored: false)
+ cell(index: 1, width: 5, height: 5, colored: false)
+ cell(index: 2, width: 5, height: 5, colored: false)
+ }
+ case "gridContainer", "coloredGridContainer":
+ let colored = mode == "coloredGridContainer"
+ VStack(spacing: 2) {
+ HStack(spacing: 2) {
+ cell(index: 0, width: 7, height: 6, colored: colored)
+ cell(index: 1, width: 7, height: 6, colored: colored)
+ }
+ HStack(spacing: 2) {
+ cell(index: 2, width: 7, height: 6, colored: colored)
+ cell(index: 3, width: 7, height: 6, colored: colored)
+ }
+ }
+ default:
+ let colored = mode == "coloredContainer"
+ HStack(alignment: .bottom, spacing: 2) {
+ cell(index: 0, width: 5, height: 8, colored: colored)
+ cell(index: 1, width: 5, height: 14, colored: colored)
+ cell(index: 2, width: 5, height: 10, colored: colored)
+ }
+ }
+ }
+
+ private func cell(index: Int, width: CGFloat, height: CGFloat, colored: Bool) -> some View {
+ let fill = fillColor(index: index, colored: colored)
+ let stroke = isSelected ? Color.white.opacity(0.95) : Color.secondary.opacity(0.35)
+ return RoundedRectangle(cornerRadius: 1.8, style: .continuous)
+ .fill(fill)
+ .overlay(
+ RoundedRectangle(cornerRadius: 1.8, style: .continuous)
+ .stroke(stroke, lineWidth: 0.8)
+ )
+ .frame(width: width, height: height)
+ }
+
+ private func fillColor(index: Int, colored: Bool) -> Color {
+ if isSelected {
+ return Color.white.opacity(colored ? 0.9 : 0.18)
+ }
+ guard colored else {
+ return Color.secondary.opacity(0.12)
+ }
+ let palette: [Color] = [.green, .purple, .blue, .orange]
+ return palette[index % palette.count].opacity(0.78)
+ }
+}
+
+private struct ShortcutHintView: View {
+ var body: some View {
+ HStack(spacing: 12) {
+ ShortcutKeycap(symbol: "⌥")
+ Text("+")
+ .font(.system(size: 22, weight: .semibold))
+ .foregroundStyle(.tertiary)
+ ShortcutKeycap(symbol: "⇧")
+ Text("+")
+ .font(.system(size: 22, weight: .semibold))
+ .foregroundStyle(.tertiary)
+ SpacebarKeycap()
+ }
+ .accessibilityLabel("⌥ ⇧ Space")
+ .allowsHitTesting(false)
+ }
+}
+
+private struct ShortcutKeycap: View {
+ let symbol: String
+
+ var body: some View {
+ Text(symbol)
+ .font(.system(size: 36, weight: .medium))
+ .foregroundStyle(.secondary)
+ .frame(width: 74, height: 58)
+ .background(KeycapBackground())
+ }
+}
+
+private struct SpacebarKeycap: View {
+ var body: some View {
+ ZStack {
+ KeycapBackground()
+ Capsule()
+ .fill(Color.secondary.opacity(0.38))
+ .frame(width: 72, height: 3)
+ .offset(y: 13)
+ }
+ .frame(width: 190, height: 58)
+ }
+}
+
+private struct KeycapBackground: View {
+ var body: some View {
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
+ .fill(Color(nsColor: .controlBackgroundColor))
+ .overlay(
+ RoundedRectangle(cornerRadius: 10, style: .continuous)
+ .stroke(Color.primary.opacity(0.12), lineWidth: 1)
+ )
+ .shadow(color: .black.opacity(0.10), radius: 8, x: 0, y: 3)
+ }
+}
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 111e5cc..cc4bdea 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -237,7 +237,6 @@
@State private var tagReorderFrames: [String: CGRect] = [:]
@State private var tagNavDragModeActive = false
@State private var tagNavDragItem: String? = nil
- @State private var tagNavDismissMonitor: Any? = nil
@State private var tagNavReorderFrames: [String: CGRect] = [:]
@State private var hoveredContainer: String? = nil // colored container lift
// Fixed interaction for "Colorless Container": hover fills persistently; click clears.
@@ -348,12 +347,6 @@
userInfo: ["active": active]
)
}
- .onChange(of: tagNavDragModeActive) { _, active in
- updateTagNavDismissMonitor(active: active)
- }
- .onDisappear {
- removeTagNavDismissMonitor()
- }
}
/// Set edit phase with synchronous notification BEFORE state change.
@@ -382,16 +375,6 @@
sideLayout
} else {
topLayout
- }
-
- if tagNavDragModeActive && tagNavDragItem == nil {
- Color.clear
- .contentShape(Rectangle())
- .ignoresSafeArea()
- .onTapGesture {
- endTagNavReorder()
- }
- .zIndex(1)
}
Button {
@@ -1271,8 +1254,6 @@
.onChanged { value in
guard canReorderTag(tagName) else { return }
switch value {
- case .first(true):
- beginTagNavReorder(tagName)
case .second(true, let drag?):
if tagNavDragItem == nil {
beginTagNavReorder(tagName)
@@ -1305,27 +1286,6 @@
}
tagNavDragModeActive = false
tagNavDragItem = nil
- removeTagNavDismissMonitor()
- }
-
- private func updateTagNavDismissMonitor(active: Bool) {
- if active {
- guard tagNavDismissMonitor == nil else { return }
- tagNavDismissMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .rightMouseDown]) { event in
- guard tagNavDragModeActive, editPhase == .none else { return event }
- endTagNavReorder()
- return nil
- }
- } else {
- removeTagNavDismissMonitor()
- }
- }
-
- private func removeTagNavDismissMonitor() {
- if let monitor = tagNavDismissMonitor {
- NSEvent.removeMonitor(monitor)
- tagNavDismissMonitor = nil
- }
}
private func reorderTagNavItem(at location: CGPoint) {
diff --git a/Apptag/Info.plist b/Apptag/Info.plist
index 39a10c1..82ab965 100644
--- a/Apptag/Info.plist
+++ b/Apptag/Info.plist
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>5.7.5</string>
+ <string>5.7.8</string>
<key>CFBundleVersion</key>
- <string>575</string>
+ <string>583</string>
<key>LSMinimumSystemVersion</key>
<string>15.0</string>
<key>NSHighResolutionCapable</key>
diff --git a/Apptag/Localization/ar-Najdi.json b/Apptag/Localization/ar-Najdi.json
index 28f133f..3c16b47 100644
--- a/Apptag/Localization/ar-Najdi.json
+++ b/Apptag/Localization/ar-Najdi.json
@@ -1,5 +1,6 @@
{
"menu.show": "ورّ TagLauncher",
+ "menu.showAppList": "ورّ قائمة التطبيقات",
"menu.preferences": "التفضيلات…",
"menu.quit": "خروج",
"menu.language": "اللغة",
@@ -13,7 +14,6 @@
"settings.about": "عن التطبيق",
"settings.launchAtLogin": "يشتغل مع الدخول",
"settings.showInDock": "إظهار في Dock",
- "settings.showMenuBarIcon": "إظهار أيقونة شريط القوائم",
"settings.appListStyle": "شكل قائمة التطبيقات:",
"settings.flat": "مسطح",
"settings.container": "حاوية بدون لون",
diff --git a/Apptag/Localization/ar.json b/Apptag/Localization/ar.json
index 787e31a..f4f3770 100644
--- a/Apptag/Localization/ar.json
+++ b/Apptag/Localization/ar.json
@@ -1,5 +1,6 @@
{
"menu.show": "إظهار TagLauncher",
+ "menu.showAppList": "إظهار قائمة التطبيقات",
"menu.preferences": "التفضيلات…",
"menu.quit": "إنهاء",
"menu.language": "اللغة",
@@ -13,7 +14,6 @@
"settings.about": "حول",
"settings.launchAtLogin": "التشغيل عند تسجيل الدخول",
"settings.showInDock": "إظهار في Dock",
- "settings.showMenuBarIcon": "إظهار أيقونة شريط القوائم",
"settings.appListStyle": "نمط قائمة التطبيقات:",
"settings.flat": "مسطح",
"settings.container": "حاوية بلا لون",
diff --git a/Apptag/Localization/cs.json b/Apptag/Localization/cs.json
index 9e13852..3ed3e51 100644
--- a/Apptag/Localization/cs.json
+++ b/Apptag/Localization/cs.json
@@ -1,5 +1,6 @@
{
"menu.show": "Zobrazit TagLauncher",
+ "menu.showAppList": "Zobrazit seznam aplikací",
"menu.preferences": "Předvolby…",
"menu.quit": "Ukončit",
"menu.language": "Jazyk",
@@ -13,7 +14,6 @@
"settings.about": "O aplikaci",
"settings.launchAtLogin": "Spustit při přihlášení",
"settings.showInDock": "Zobrazit v Docku",
- "settings.showMenuBarIcon": "Zobrazit ikonu v řádku nabídek",
"settings.appListStyle": "Styl seznamu aplikací:",
"settings.flat": "Plochý",
"settings.container": "Bezbarvý kontejner",
diff --git a/Apptag/Localization/da.json b/Apptag/Localization/da.json
index 742c04f..85105b8 100644
--- a/Apptag/Localization/da.json
+++ b/Apptag/Localization/da.json
@@ -1,5 +1,6 @@
{
"menu.show": "Vis TagLauncher",
+ "menu.showAppList": "Vis appliste",
"menu.preferences": "Indstillinger…",
"menu.quit": "Slut",
"menu.language": "Sprog",
@@ -13,7 +14,6 @@
"settings.about": "Om",
"settings.launchAtLogin": "Start ved login",
"settings.showInDock": "Vis i Dock",
- "settings.showMenuBarIcon": "Vis ikon i menulinjen",
"settings.appListStyle": "Applistestil:",
"settings.flat": "Flad",
"settings.container": "Farveløs beholder",
diff --git a/Apptag/Localization/de.json b/Apptag/Localization/de.json
index 1648ee7..c873c77 100644
--- a/Apptag/Localization/de.json
+++ b/Apptag/Localization/de.json
@@ -1,5 +1,6 @@
{
"menu.show": "TagLauncher anzeigen",
+ "menu.showAppList": "App-Liste anzeigen",
"menu.preferences": "Einstellungen…",
"menu.quit": "Beenden",
"menu.language": "Sprache",
@@ -13,7 +14,6 @@
"settings.about": "Über",
"settings.launchAtLogin": "Beim Anmelden starten",
"settings.showInDock": "Im Dock anzeigen",
- "settings.showMenuBarIcon": "Menüleistensymbol anzeigen",
"settings.appListStyle": "App-Listenstil:",
"settings.flat": "Flach",
"settings.container": "Farbloser Container",
diff --git a/Apptag/Localization/en.json b/Apptag/Localization/en.json
index 12f774a..cf572bf 100644
--- a/Apptag/Localization/en.json
+++ b/Apptag/Localization/en.json
@@ -1,5 +1,6 @@
{
"menu.show": "Show TagLauncher",
+ "menu.showAppList": "Show App List",
"menu.preferences": "Preferences…",
"menu.quit": "Quit",
"menu.language": "Language",
@@ -13,7 +14,6 @@
"settings.about": "About",
"settings.launchAtLogin": "Launch at login",
"settings.showInDock": "Show in Dock",
- "settings.showMenuBarIcon": "Show menu bar icon",
"settings.appListStyle": "App list style:",
"settings.flat": "Flat",
"settings.container": "Colorless Container",
diff --git a/Apptag/Localization/es.json b/Apptag/Localization/es.json
index ed11db5..385f2d0 100644
--- a/Apptag/Localization/es.json
+++ b/Apptag/Localization/es.json
@@ -1,5 +1,6 @@
{
"menu.show": "Mostrar TagLauncher",
+ "menu.showAppList": "Mostrar lista de apps",
"menu.preferences": "Preferencias…",
"menu.quit": "Salir",
"menu.language": "Idioma",
@@ -13,7 +14,6 @@
"settings.about": "Acerca de",
"settings.launchAtLogin": "Iniciar al iniciar sesión",
"settings.showInDock": "Mostrar en el Dock",
- "settings.showMenuBarIcon": "Mostrar icono en la barra de menús",
"settings.appListStyle": "Estilo de lista de apps:",
"settings.flat": "Plano",
"settings.container": "Contenedor sin color",
diff --git a/Apptag/Localization/fr.json b/Apptag/Localization/fr.json
index 11e094e..cbe2e65 100644
--- a/Apptag/Localization/fr.json
+++ b/Apptag/Localization/fr.json
@@ -1,5 +1,6 @@
{
"menu.show": "Afficher TagLauncher",
+ "menu.showAppList": "Afficher la liste des apps",
"menu.preferences": "Préférences…",
"menu.quit": "Quitter",
"menu.language": "Langue",
@@ -13,7 +14,6 @@
"settings.about": "À propos",
"settings.launchAtLogin": "Lancer à l'ouverture de session",
"settings.showInDock": "Afficher dans le Dock",
- "settings.showMenuBarIcon": "Afficher l’icône de la barre des menus",
"settings.appListStyle": "Style de liste d'applications :",
"settings.flat": "Liste",
"settings.container": "Conteneur sans couleur",
diff --git a/Apptag/Localization/id.json b/Apptag/Localization/id.json
index 4529646..c5f5048 100644
--- a/Apptag/Localization/id.json
+++ b/Apptag/Localization/id.json
@@ -1,5 +1,6 @@
{
"menu.show": "Tampilkan TagLauncher",
+ "menu.showAppList": "Tampilkan daftar app",
"menu.preferences": "Preferensi…",
"menu.quit": "Keluar",
"menu.language": "Bahasa",
@@ -13,7 +14,6 @@
"settings.about": "Tentang",
"settings.launchAtLogin": "Mulai saat masuk",
"settings.showInDock": "Tampilkan di Dock",
- "settings.showMenuBarIcon": "Tampilkan ikon bilah menu",
"settings.appListStyle": "Gaya daftar app:",
"settings.flat": "Datar",
"settings.container": "Kontainer tanpa warna",
diff --git a/Apptag/Localization/it.json b/Apptag/Localization/it.json
index a73ed69..0f20df1 100644
--- a/Apptag/Localization/it.json
+++ b/Apptag/Localization/it.json
@@ -1,5 +1,6 @@
{
"menu.show": "Mostra TagLauncher",
+ "menu.showAppList": "Mostra elenco app",
"menu.preferences": "Preferenze…",
"menu.quit": "Esci",
"menu.language": "Lingua",
@@ -13,7 +14,6 @@
"settings.about": "Informazioni",
"settings.launchAtLogin": "Avvia al login",
"settings.showInDock": "Mostra nel Dock",
- "settings.showMenuBarIcon": "Mostra icona nella barra dei menu",
"settings.appListStyle": "Stile elenco app:",
"settings.flat": "Elenco",
"settings.container": "Contenitore senza colore",
diff --git a/Apptag/Localization/ja.json b/Apptag/Localization/ja.json
index 57b51c6..146eb1d 100644
--- a/Apptag/Localization/ja.json
+++ b/Apptag/Localization/ja.json
@@ -1,5 +1,6 @@
{
"menu.show": "TagLauncherを表示",
+ "menu.showAppList": "アプリ一覧を表示",
"menu.preferences": "設定…",
"menu.quit": "終了",
"menu.language": "言語",
@@ -13,7 +14,6 @@
"settings.about": "概要",
"settings.launchAtLogin": "ログイン時に起動",
"settings.showInDock": "Dockに表示",
- "settings.showMenuBarIcon": "メニューバーアイコンを表示",
"settings.appListStyle": "アプリ一覧スタイル:",
"settings.flat": "フラット",
"settings.container": "無色コンテナ",
diff --git a/Apptag/Localization/ko.json b/Apptag/Localization/ko.json
index 1a0a4ce..0159799 100644
--- a/Apptag/Localization/ko.json
+++ b/Apptag/Localization/ko.json
@@ -1,5 +1,6 @@
{
"menu.show": "TagLauncher 보기",
+ "menu.showAppList": "앱 목록 보기",
"menu.preferences": "환경설정…",
"menu.quit": "종료",
"menu.language": "언어",
@@ -13,7 +14,6 @@
"settings.about": "정보",
"settings.launchAtLogin": "로그인 시 시작",
"settings.showInDock": "Dock에 표시",
- "settings.showMenuBarIcon": "메뉴 막대 아이콘 표시",
"settings.appListStyle": "앱 목록 스타일:",
"settings.flat": "플랫",
"settings.container": "무색 컨테이너",
diff --git a/Apptag/Localization/ms.json b/Apptag/Localization/ms.json
index 42c45d4..d547d15 100644
--- a/Apptag/Localization/ms.json
+++ b/Apptag/Localization/ms.json
@@ -1,5 +1,6 @@
{
"menu.show": "Tunjukkan TagLauncher",
+ "menu.showAppList": "Tunjukkan senarai app",
"menu.preferences": "Keutamaan…",
"menu.quit": "Keluar",
"menu.language": "Bahasa",
@@ -13,7 +14,6 @@
"settings.about": "Tentang",
"settings.launchAtLogin": "Mula semasa log masuk",
"settings.showInDock": "Tunjukkan dalam Dock",
- "settings.showMenuBarIcon": "Tunjukkan ikon bar menu",
"settings.appListStyle": "Gaya senarai app:",
"settings.flat": "Datar",
"settings.container": "Kontainer tanpa warna",
diff --git a/Apptag/Localization/nb.json b/Apptag/Localization/nb.json
index 3d1956a..02c1e47 100644
--- a/Apptag/Localization/nb.json
+++ b/Apptag/Localization/nb.json
@@ -1,5 +1,6 @@
{
"menu.show": "Vis TagLauncher",
+ "menu.showAppList": "Vis appliste",
"menu.preferences": "Innstillinger…",
"menu.quit": "Avslutt",
"menu.language": "Språk",
@@ -13,7 +14,6 @@
"settings.about": "Om",
"settings.launchAtLogin": "Start ved innlogging",
"settings.showInDock": "Vis i Dock",
- "settings.showMenuBarIcon": "Vis menylinjeikon",
"settings.appListStyle": "App-listestil:",
"settings.flat": "Flat",
"settings.container": "Fargeløs beholder",
diff --git a/Apptag/Localization/nl.json b/Apptag/Localization/nl.json
index 410a346..2c1fcf8 100644
--- a/Apptag/Localization/nl.json
+++ b/Apptag/Localization/nl.json
@@ -1,5 +1,6 @@
{
"menu.show": "Toon TagLauncher",
+ "menu.showAppList": "Toon applijst",
"menu.preferences": "Voorkeuren…",
"menu.quit": "Stop",
"menu.language": "Taal",
@@ -13,7 +14,6 @@
"settings.about": "Over",
"settings.launchAtLogin": "Start bij inloggen",
"settings.showInDock": "Toon in Dock",
- "settings.showMenuBarIcon": "Toon menubalkicoon",
"settings.appListStyle": "App-lijststijl:",
"settings.flat": "Plat",
"settings.container": "Kleurloze container",
diff --git a/Apptag/Localization/nn.json b/Apptag/Localization/nn.json
index 7398a8c..155bf42 100644
--- a/Apptag/Localization/nn.json
+++ b/Apptag/Localization/nn.json
@@ -1,5 +1,6 @@
{
"menu.show": "Vis TagLauncher",
+ "menu.showAppList": "Vis appliste",
"menu.preferences": "Innstillingar…",
"menu.quit": "Avslutt",
"menu.language": "Språk",
@@ -13,7 +14,6 @@
"settings.about": "Om",
"settings.launchAtLogin": "Start ved innlogging",
"settings.showInDock": "Vis i Dock",
- "settings.showMenuBarIcon": "Vis menylinjeikon",
"settings.appListStyle": "App-listestil:",
"settings.flat": "Flat",
"settings.container": "Fargeløs beholder",
diff --git a/Apptag/Localization/no.json b/Apptag/Localization/no.json
index 3d1956a..02c1e47 100644
--- a/Apptag/Localization/no.json
+++ b/Apptag/Localization/no.json
@@ -1,5 +1,6 @@
{
"menu.show": "Vis TagLauncher",
+ "menu.showAppList": "Vis appliste",
"menu.preferences": "Innstillinger…",
"menu.quit": "Avslutt",
"menu.language": "Språk",
@@ -13,7 +14,6 @@
"settings.about": "Om",
"settings.launchAtLogin": "Start ved innlogging",
"settings.showInDock": "Vis i Dock",
- "settings.showMenuBarIcon": "Vis menylinjeikon",
"settings.appListStyle": "App-listestil:",
"settings.flat": "Flat",
"settings.container": "Fargeløs beholder",
diff --git a/Apptag/Localization/pl.json b/Apptag/Localization/pl.json
index c381df8..de7c8f6 100644
--- a/Apptag/Localization/pl.json
+++ b/Apptag/Localization/pl.json
@@ -1,5 +1,6 @@
{
"menu.show": "Pokaż TagLauncher",
+ "menu.showAppList": "Pokaż listę aplikacji",
"menu.preferences": "Preferencje…",
"menu.quit": "Zakończ",
"menu.language": "Język",
@@ -13,7 +14,6 @@
"settings.about": "O aplikacji",
"settings.launchAtLogin": "Uruchamiaj przy logowaniu",
"settings.showInDock": "Pokaż w Docku",
- "settings.showMenuBarIcon": "Pokaż ikonę na pasku menu",
"settings.appListStyle": "Styl listy aplikacji:",
"settings.flat": "Płaski",
"settings.container": "Bezbarwny kontener",
diff --git a/Apptag/Localization/pt-BR.json b/Apptag/Localization/pt-BR.json
index 24777a2..35f43e9 100644
--- a/Apptag/Localization/pt-BR.json
+++ b/Apptag/Localization/pt-BR.json
@@ -1,5 +1,6 @@
{
"menu.show": "Mostrar TagLauncher",
+ "menu.showAppList": "Mostrar lista de apps",
"menu.preferences": "Preferências…",
"menu.quit": "Sair",
"menu.language": "Idioma",
@@ -13,7 +14,6 @@
"settings.about": "Sobre",
"settings.launchAtLogin": "Iniciar ao fazer login",
"settings.showInDock": "Mostrar no Dock",
- "settings.showMenuBarIcon": "Mostrar ícone na barra de menus",
"settings.appListStyle": "Estilo da lista de apps:",
"settings.flat": "Plano",
"settings.container": "Contêiner sem cor",
diff --git a/Apptag/Localization/ro.json b/Apptag/Localization/ro.json
index 33a2930..6f2fa9f 100644
--- a/Apptag/Localization/ro.json
+++ b/Apptag/Localization/ro.json
@@ -1,5 +1,6 @@
{
"menu.show": "Afișează TagLauncher",
+ "menu.showAppList": "Afișează lista aplicațiilor",
"menu.preferences": "Preferințe…",
"menu.quit": "Ieșire",
"menu.language": "Limbă",
@@ -13,7 +14,6 @@
"settings.about": "Despre",
"settings.launchAtLogin": "Pornește la autentificare",
"settings.showInDock": "Afișează în Dock",
- "settings.showMenuBarIcon": "Afișează pictograma în bara de meniu",
"settings.appListStyle": "Stil listă aplicații:",
"settings.flat": "Plat",
"settings.container": "Container fără culoare",
diff --git a/Apptag/Localization/ru.json b/Apptag/Localization/ru.json
index 11b379c..71801d2 100644
--- a/Apptag/Localization/ru.json
+++ b/Apptag/Localization/ru.json
@@ -1,5 +1,6 @@
{
"menu.show": "Показать TagLauncher",
+ "menu.showAppList": "Показать список приложений",
"menu.preferences": "Настройки…",
"menu.quit": "Выйти",
"menu.language": "Язык",
@@ -13,7 +14,6 @@
"settings.about": "О программе",
"settings.launchAtLogin": "Запускать при входе",
"settings.showInDock": "Показывать в Dock",
- "settings.showMenuBarIcon": "Показывать значок в строке меню",
"settings.appListStyle": "Стиль списка приложений:",
"settings.flat": "Плоский",
"settings.container": "Бесцветный контейнер",
diff --git a/Apptag/Localization/sr-Cyrl.json b/Apptag/Localization/sr-Cyrl.json
index 73a3d80..b5a1238 100644
--- a/Apptag/Localization/sr-Cyrl.json
+++ b/Apptag/Localization/sr-Cyrl.json
@@ -1,5 +1,6 @@
{
"menu.show": "Прикажи TagLauncher",
+ "menu.showAppList": "Прикажи листу апликација",
"menu.preferences": "Подешавања…",
"menu.quit": "Изађи",
"menu.language": "Језик",
@@ -13,7 +14,6 @@
"settings.about": "О програму",
"settings.launchAtLogin": "Покрени при пријављивању",
"settings.showInDock": "Прикажи у Dock-у",
- "settings.showMenuBarIcon": "Прикажи икону у траци менија",
"settings.appListStyle": "Стил листе апликација:",
"settings.flat": "Равно",
"settings.container": "Безбојни контејнер",
diff --git a/Apptag/Localization/sv.json b/Apptag/Localization/sv.json
index b7ed5f3..13ab7f0 100644
--- a/Apptag/Localization/sv.json
+++ b/Apptag/Localization/sv.json
@@ -1,5 +1,6 @@
{
"menu.show": "Visa TagLauncher",
+ "menu.showAppList": "Visa applista",
"menu.preferences": "Inställningar…",
"menu.quit": "Avsluta",
"menu.language": "Språk",
@@ -13,7 +14,6 @@
"settings.about": "Om",
"settings.launchAtLogin": "Starta vid inloggning",
"settings.showInDock": "Visa i Dock",
- "settings.showMenuBarIcon": "Visa ikon i menyraden",
"settings.appListStyle": "Applistestil:",
"settings.flat": "Platt",
"settings.container": "Färglös behållare",
diff --git a/Apptag/Localization/th.json b/Apptag/Localization/th.json
index 8583d63..05274ad 100644
--- a/Apptag/Localization/th.json
+++ b/Apptag/Localization/th.json
@@ -1,5 +1,6 @@
{
"menu.show": "แสดง TagLauncher",
+ "menu.showAppList": "แสดงรายการแอป",
"menu.preferences": "การตั้งค่า…",
"menu.quit": "ออก",
"menu.language": "ภาษา",
@@ -13,7 +14,6 @@
"settings.about": "เกี่ยวกับ",
"settings.launchAtLogin": "เปิดเมื่อเข้าสู่ระบบ",
"settings.showInDock": "แสดงใน Dock",
- "settings.showMenuBarIcon": "แสดงไอคอนในแถบเมนู",
"settings.appListStyle": "รูปแบบรายการแอป:",
"settings.flat": "แบน",
"settings.container": "คอนเทนเนอร์ไม่มีสี",
diff --git a/Apptag/Localization/tr.json b/Apptag/Localization/tr.json
index 03f5bc9..0226153 100644
--- a/Apptag/Localization/tr.json
+++ b/Apptag/Localization/tr.json
@@ -1,5 +1,6 @@
{
"menu.show": "TagLauncher’ı Göster",
+ "menu.showAppList": "Uygulama listesini göster",
"menu.preferences": "Tercihler…",
"menu.quit": "Çık",
"menu.language": "Dil",
@@ -13,7 +14,6 @@
"settings.about": "Hakkında",
"settings.launchAtLogin": "Girişte başlat",
"settings.showInDock": "Dock’ta göster",
- "settings.showMenuBarIcon": "Menü çubuğu simgesini göster",
"settings.appListStyle": "Uygulama listesi stili:",
"settings.flat": "Düz",
"settings.container": "Renksiz Kapsayıcı",
diff --git a/Apptag/Localization/uk.json b/Apptag/Localization/uk.json
index 8b14a62..d11f823 100644
--- a/Apptag/Localization/uk.json
+++ b/Apptag/Localization/uk.json
@@ -1,5 +1,6 @@
{
"menu.show": "Показати TagLauncher",
+ "menu.showAppList": "Показати список програм",
"menu.preferences": "Параметри…",
"menu.quit": "Вийти",
"menu.language": "Мова",
@@ -13,7 +14,6 @@
"settings.about": "Про програму",
"settings.launchAtLogin": "Запускати під час входу",
"settings.showInDock": "Показувати в Dock",
- "settings.showMenuBarIcon": "Показувати значок у рядку меню",
"settings.appListStyle": "Стиль списку застосунків:",
"settings.flat": "Плоский",
"settings.container": "Безбарвний контейнер",
diff --git a/Apptag/Localization/vi.json b/Apptag/Localization/vi.json
index 7a33821..f4add4a 100644
--- a/Apptag/Localization/vi.json
+++ b/Apptag/Localization/vi.json
@@ -1,5 +1,6 @@
{
"menu.show": "Hiển thị TagLauncher",
+ "menu.showAppList": "Hiển thị danh sách ứng dụng",
"menu.preferences": "Tùy chọn…",
"menu.quit": "Thoát",
"menu.language": "Ngôn ngữ",
@@ -13,7 +14,6 @@
"settings.about": "Giới thiệu",
"settings.launchAtLogin": "Khởi động khi đăng nhập",
"settings.showInDock": "Hiển thị trong Dock",
- "settings.showMenuBarIcon": "Hiển thị biểu tượng trên thanh menu",
"settings.appListStyle": "Kiểu danh sách ứng dụng:",
"settings.flat": "Phẳng",
"settings.container": "Vùng chứa không màu",
diff --git a/Apptag/Localization/zh-Hans.json b/Apptag/Localization/zh-Hans.json
index ce635c1..9134421 100644
--- a/Apptag/Localization/zh-Hans.json
+++ b/Apptag/Localization/zh-Hans.json
@@ -1,5 +1,6 @@
{
"menu.show": "显示 TagLauncher",
+ "menu.showAppList": "显示应用列表",
"menu.preferences": "偏好设置…",
"menu.quit": "退出",
"menu.language": "语言",
@@ -13,7 +14,6 @@
"settings.about": "关于",
"settings.launchAtLogin": "登录时启动",
"settings.showInDock": "在 Dock 中显示",
- "settings.showMenuBarIcon": "显示菜单栏图标",
"settings.appListStyle": "应用列表样式:",
"settings.flat": "平铺",
"settings.container": "无色容器",
diff --git a/Apptag/Localization/zh-Hant.json b/Apptag/Localization/zh-Hant.json
index 1fd7413..3258d52 100644
--- a/Apptag/Localization/zh-Hant.json
+++ b/Apptag/Localization/zh-Hant.json
@@ -1,5 +1,6 @@
{
"menu.show": "顯示 TagLauncher",
+ "menu.showAppList": "顯示應用列表",
"menu.preferences": "偏好設定…",
"menu.quit": "退出",
"menu.language": "語言",
@@ -13,7 +14,6 @@
"settings.about": "關於",
"settings.launchAtLogin": "登入時啟動",
"settings.showInDock": "在 Dock 中顯示",
- "settings.showMenuBarIcon": "顯示選單列圖示",
"settings.appListStyle": "應用程式列表樣式:",
"settings.flat": "平鋪",
"settings.container": "無色容器",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abdc866..194c64b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,91 @@
# Apptag Changelog
+## [5.7.8] — 2026-05-15
+
+- 修复 TagLauncher 浮层每次打开都会重复添加 Escape 键本地监听的问题,关闭浮层时会同步释放监听器,避免事件监听累积
+- 清理普通应用列表中标签拖动排序的旧兜底逻辑,移除可能吞掉下一次点击的鼠标监听器和不可达透明点击层
+- 菜单栏图标改为强制显示,移除设置页里的「显示菜单栏图标」开关,并从全部 29 个语言包同步删除该设置文案
+- 清理 macOS 应用主菜单中的 Services / Hide / Hide Others / Show All 默认项,新增「显示应用列表」菜单项
+- 顶部应用菜单和状态栏菜单的「显示应用列表」均使用全语言本地化文案,并用 `⌥⇧␣` 图形符号展示快捷键
+- 用 SwiftUI `CommandGroup` 从源头替换 macOS 默认 Services/Hide 菜单组,并增加延迟重试清理,避免系统菜单重建后恢复旧菜单项
+- 菜单栏状态项改为固定方形长度并强制 `isVisible`,降低图标被系统计算成不可见宽度的风险
+- 优化 Dock 显示设置刷新逻辑:只有 `showDockIcon` 变化时才更新系统 activation policy,避免无关设置变更触发全局刷新
+- 菜单栏图标改为代码自绘 template 图标,不再依赖 SF Symbol 加载,避免系统符号渲染失败导致图标不可见
+- 优化「通用」设置页排版:保留顶部三个开关横排,将应用列表样式改为两列选择块,并统一后续设置行的标签列、控件列和说明文字左边缘
+- 微调「应用列表样式」选择区:Flat 单独占满第一行,其余四个样式按两列两行排列,形成更稳定的视觉宽度关系
+- 收窄「应用列表样式」选项块高度,并为平铺、瀑布流容器、等高网格增加小图示,帮助用户快速区分布局类型
+- 在设置页的「语言」「数据」「关于」三个空白较多的页签增加 `⌥ + ⇧ + 空格键` 图形化快捷键提示,避免影响「通用」和「标签」页的主要设置/编辑区域
+- 版本号保持 `5.7.8`,Build 更新为 `583`
+- 同步更新 `AppStore_Submission.md` 的版本信息和 What's New
+
+## [5.7.5] — 2026-05-14
+
+- 新增「显示菜单栏图标」设置项,默认开启,并放在「在 Dock 中显示」之前
+- 修复菜单栏小图标不显示或被系统符号渲染不稳定的问题,改为自绘 template 状态栏图标
+- 修复全屏应用 Space 中打开设置时,设置窗口跑到其他窗口/屏幕的问题:设置窗口作为当前 TagLauncher overlay 的子窗口置顶显示
+- 优化设置窗口关闭后 overlay 的焦点恢复,减少设置窗口和主浮层之间的层级错乱
+- 为普通应用列表中的标签导航增加拖动排序退出兜底,尝试解决拖动后标签持续抖动的问题
+- 版本号更新为 `5.7.5`,Build 更新为 `575`
+
+## [5.7.0] — 2026-05-13
+
+- 扩展语言支持到 29 个语言版本,新增法语、德语、巴西葡萄牙语、泰语、越南语、阿拉伯语、土耳其语、印尼语、捷克语、丹麦语、荷兰语、挪威语、马来语、波兰语、罗马尼亚语、瑞典语等
+- 中文保持简体/繁体支持,不增加广东话版本
+- 设置页「语言」页签改为两列纵向排版,适配语言数量增加后的空间压力
+- 优化语言自动匹配逻辑,支持更多系统语言代码映射
+- 修复状态栏菜单项在语言切换后的刷新逻辑,避免重复创建状态栏图标
+- 固定设置窗口尺寸为 880×460,减少不同页签之间的高度和宽度跳动
+- 版本号更新为 `5.7.0`,Build 更新为 `570`
+
+## [5.6.0] — 2026-05-13
+
+- 修复编辑模式下左侧标签拖动排序失效的问题,改用自定义手势和几何位置检测实现实时排序
+- 标签拖动排序增加动态动画反馈,拖动时左侧标签顺序和右侧对应标签容器同步变化
+- 普通应用列表视图中的顶部/左侧/右侧标签导航支持长按拖动排序
+- 标签排序逻辑覆盖全部 5 种应用列表样式,保证标签导航顺序和应用分组布局一致
+- 修复导入/导出分类与布局功能,失败时显示明确错误提示
+- App Store 沙盒构建下隐藏 Launch at Login 控件,避免沙盒环境中不可用功能误导用户
+- 移除已弃用的应用启动 API,统一使用现代 `NSWorkspace.OpenConfiguration`
+- 新增 App Store 沙盒 entitlements,支持 App Store 构建验证
+- 版本号更新为 `5.6.0`,Build 更新为 `560`
+
+## [5.5.0] — 2026-05-11
+
+- 新增设置页第一个页签「语言」,原有「通用 / 标签 / 数据 / 关于」整体后移
+- 支持在设置页直接切换语言,菜单和设置面板实时同步刷新
+- 语言切换时增加 macOS 风格的全局刷新过渡图标,降低界面瞬间重建的突兀感
+- 设置窗口默认宽度扩展到 880pt,并在当前 TagLauncher 应用列表视图中央浮动置顶
+- 多屏使用时,设置窗口会优先居中显示在当前 TagLauncher overlay 所在屏幕
+- 数据页按钮文案由「导出 tag / 导入 tag」改为「导出分类与布局 / 导入分类与布局」,并同步所有已支持语言
+- 增加旧版非沙盒数据到 App Store 沙盒容器的数据迁移逻辑,降低更新后标签和排序丢失风险
+- 新增 App Store 提审资料文档,记录版本信息、隐私说明、截图建议和审核备注
+- 版本号更新为 `5.5.0`,Build 更新为 `550`
+
+## [5.2.0] — 2026-05-11
+
+- 重做 App 拖动预览实现,改为 CALayer / AppKit 预览层方式,提升拖动过程的稳定性和跟手感
+- 优化拖动预览在 overlay 和全屏 Space 中的显示层级,减少拖动图标被遮挡或丢失的情况
+- 保持拖动投放逻辑与分类容器绑定,继续支持移动/复制到目标标签
+- 版本号更新为 `5.2.0`,Build 更新为 `520`
+
+## [5.1.12] — 2026-05-11
+
+- 新增 `AppDragCoordinator`,集中管理 App 图标拖动预览、投放目标注册和拖放完成回调
+- 优化 App 图标网格项尺寸计算,减少隐藏/显示 App 名称时的布局抖动
+- 新增 `TagGroupView`,拆分标签分组容器视图逻辑,降低主视图复杂度
+- 优化标签数据库导入/导出和排序数据结构处理
+- 调整图标资源生成流程,删除旧 `AppIcon.iconset`,使用压缩后的 `icon-icns.icns` 降低包体体积
+- 补充应用描述相关本地化文案
+- 版本号更新为 `5.1.12`,Build 更新为 `512`
+
+## [4.0.7] — 2026-05-11
+
+- 调整应用列表布局和 App 启动逻辑,修复部分网格容器视图下点击 App 后启动/关闭 overlay 不稳定的问题
+- 更新 9 个既有语言的视图名称和说明文案
+- 完善 `build.sh`,强化构建流程中的 bundle 结构、资源复制、权限设置、strip 和签名步骤
+- 新增 `make_dmg.sh`,支持生成 DMG 发布包
+- 版本号更新为 `4.0.7`,Build 更新为 `407`
+
## [4.0.0] — 2026-05-10
- 新增「无色网格容器」和「彩色网格容器」两种 App 列表视图样式
diff --git a/plan-deepthink.md b/plan-deepthink.md
new file mode 100644
index 0000000..2c59e6a
--- /dev/null
+++ b/plan-deepthink.md
@@ -0,0 +1,262 @@
+> 2026.0511.18:02已创建:plan-deepthink.md
+>
+> 加入了一个小章节:
+>
+> macOS Backward Compatibility
+>
+> 内容包括:
+>
+> - 是否可能兼容 macOS 10.15 的结论
+> - 当前主要阻碍
+> - 编译探测结果
+> - 推荐路线
+> - 未来迁移任务清单
+>
+> 后续关于未来版本、长期功能、架构想法、兼容性计划,都可以继续追加到这个文件里。当前这个文件还未提交进 git。
+
+# Apptag Future Development Notes
+
+This document is the shared parking lot for future-facing ideas, deeper product thoughts, and technical plans that should not interrupt the current release flow.
+
+When a new future idea comes up, add it here first so it can be revisited, compared, and prioritized later.
+
+## macOS Backward Compatibility
+
+### Question
+
+Can Apptag eventually support older macOS versions, especially macOS 10.15 Catalina?
+
+### Current Conclusion
+
+Supporting macOS 10.15 is theoretically possible, but it is not a simple deployment-target change. The current codebase is built around newer SwiftUI and AppKit APIs, so macOS 10.15 support would require a dedicated compatibility pass and likely a separate branch.
+
+For the near-term App Store release, keeping the current macOS 15.0 minimum is safer.
+
+### Main Compatibility Blockers
+
+- The build currently targets only Apple Silicon:
+ - `arm64-apple-macosx15.0`
+ - macOS 10.15 only runs on Intel Macs, so a 10.15-compatible release would need an `x86_64-apple-macosx10.15` build, ideally packaged as a universal app when also supporting Apple Silicon.
+- The app currently uses the SwiftUI app lifecycle:
+ - `@main struct TagLauncherApp: App`
+ - `Settings { PreferencesView() }`
+ - This lifecycle is macOS 11+, so macOS 10.15 would require returning to an AppKit lifecycle with `NSApplicationDelegate`, manually managed preferences windows, and `NSHostingView`.
+- The UI uses many SwiftUI APIs that are not available on macOS 10.15, including:
+ - `LazyVGrid`
+ - `LazyVStack`
+ - `@AppStorage`
+ - `Label`
+ - `ProgressView`
+ - Newer `.onChange` signatures
+ - `.foregroundStyle`
+ - `.ultraThinMaterial` / `.ultraThickMaterial`
+ - `.controlSize`
+ - `symbolRenderingMode`
+- Several system APIs would need fallbacks:
+ - `NSImage(systemSymbolName:)` is macOS 11+.
+ - `UniformTypeIdentifiers` / `UTType.plainText` should be replaced or wrapped for older systems.
+ - `NSOpenPanel.allowedContentTypes = [.json]` should fall back to `allowedFileTypes = ["json"]`.
+ - `NSScreen.safeAreaInsets` should be guarded or removed for 10.15, where notch handling is not relevant.
+
+### Compile Probe Result
+
+A direct compile probe with:
+
+```bash
+swiftc -o /private/tmp/TagLauncher-compat-check \
+ -framework AppKit \
+ -framework SwiftUI \
+ -framework Carbon \
+ -sdk "$(xcrun --sdk macosx --show-sdk-path)" \
+ -target arm64-apple-macosx10.15 \
+ Apptag/*.swift
+```
+
+failed immediately on macOS 14+ API usage:
+
+```text
+'onChange(of:initial:_:)' is only available in macOS 14.0 or newer
+```
+
+That is only the first visible blocker; more availability issues would appear after fixing it.
+
+### Recommended Path
+
+- Do not block the 5.5.0 App Store release on macOS 10.15 compatibility.
+- If broader compatibility becomes a product goal, create a dedicated branch such as `compat/macos-10.15`.
+- Consider an intermediate target first:
+ - macOS 13 or 14: likely lower effort.
+ - macOS 10.15: larger migration, because it requires lifecycle, layout, storage-binding, symbol, drag/drop, and file-panel compatibility work.
+
+### Possible Migration Tasks
+
+- Add universal build support:
+ - `x86_64-apple-macosx10.15`
+ - `arm64-apple-macosx11.0+`
+ - merge with `lipo` or migrate to an Xcode project / Swift Package build flow.
+- Replace SwiftUI app lifecycle with an AppKit-compatible entrypoint.
+- Wrap or replace newer SwiftUI APIs with compatibility components.
+- Replace SF Symbol dependencies with bundled vector or PNG assets for older systems.
+- Add availability guards for file panels, screen safe areas, app launching, and drag/drop type identifiers.
+- Maintain a compatibility test checklist on an actual macOS 10.15 Intel machine or VM.
+
+## AI-Assisted App Categorization
+
+### Product Goal
+
+Help users create a useful TagLauncher category layout with very low effort. The user should be able to describe the desired tag names in natural language, then receive a proposed tag/category layout for the apps installed on their Mac.
+
+### Version Plan
+
+#### TagLauncher 6.0.0: Local Category Creator
+
+Ship a local-only version first. This version does not use a server and does not perform intelligent app assignment.
+
+Goal:
+
+- let the user speak or type desired category/tag names
+- parse the input into short tag chips
+- create those tags locally
+- append them to `tagOrder`
+- leave app assignment to the existing manual/editing workflow
+
+Why this belongs in 6.0.0:
+
+- no backend dependency
+- no LLM cost
+- no upload/privacy review burden
+- validates the entry point, wording, parsing, and user workflow before adding AI
+- gives users immediate value even before intelligent classification exists
+
+Suggested interaction:
+
+1. User opens "Create Categories" from Preferences > Tags.
+2. User types or dictates a messy list such as "工作 设计 写作 娱乐 系统工具".
+3. The app parses it into chips.
+4. The user can rename/delete/reorder chips.
+5. The app creates missing tags locally and preserves existing tags.
+6. The app shows a short success state and points the user back to edit mode for app assignment.
+
+Implementation notes:
+
+- Use macOS dictation into a regular text field; no custom audio handling in 6.0.0.
+- Accept separators such as newline, comma, Chinese comma, semicolon, slash, bullet, and spaces for short CJK phrases.
+- Trim whitespace and punctuation.
+- Reject empty labels and duplicates.
+- Warn when labels are too long.
+- Do not delete or overwrite existing tags.
+- Assign colors by cycling through `TagColor.allIndices`.
+- Save through the existing `TagDatabase.save` path.
+
+#### Later Version: AI Categorization Draft
+
+After 6.0.0 proves the local category-creation flow, add the server-backed AI draft feature.
+
+### Recommended AI Version
+
+Do not make the first version fully automatic. Use a trust-building flow:
+
+1. User opens an "AI Categorize" entry from Preferences > Tags or the overlay edit mode.
+2. App shows a compact dialog with:
+ - a text input for category names or natural-language intent
+ - suggested starter chips such as Work, Design, Writing, Development, Study, Entertainment, Utilities
+ - a clear privacy note explaining that app names, paths, bundle identifiers, and the user's requested category names will be uploaded only after confirmation
+3. User confirms upload.
+4. Server returns a draft JSON plan.
+5. App displays a preview:
+ - new tags
+ - tag order
+ - each app's proposed tag assignment
+ - conflicts and uncertain apps
+6. User can apply, edit, or cancel.
+7. Applying the plan writes through `TagDatabase.save`, after creating a local backup snapshot so the change can be undone.
+
+### Client Data To Send
+
+Send only the minimum needed for classification:
+
+- app display name
+- bundle identifier
+- install path category, for example `/Applications`, `/System/Applications`, `~/Applications`
+- current tags, if any
+- user-provided desired category names or intent
+- current app language
+
+Avoid uploading icons, full user paths beyond the app path already needed for local mapping, unrelated files, or usage history.
+
+### Server Strategy
+
+The backend does not need a heavy agent. It can be a small stateless API:
+
+- `POST /v1/categorization-plans`
+- validates input
+- normalizes category names
+- calls one lightweight LLM with a strict JSON schema
+- validates the returned JSON
+- returns a draft plan with confidence and reasons
+
+Use a provider-adapter layer so the app is not locked to one model vendor. For the first version, the model should output only structured JSON; no tool use, no browsing, no remote code execution.
+
+### Cost Strategy
+
+This is a low-token, single-turn classification problem. A typical request can stay small by sending app names, bundle IDs, and 5-15 desired categories. The cost-effective path is:
+
+- start with a cheap mini/flash/lite model
+- use strict JSON schema output
+- retry once with a stronger model only if validation fails or confidence is low
+- cache common app classification hints server-side by bundle identifier
+- optionally pre-classify well-known bundle IDs locally before calling the model
+
+### Output Plan Shape
+
+The server should return a draft, not a final overwrite:
+
+```json
+{
+ "version": 1,
+ "tags": [
+ { "name": "Design", "color": 1 }
+ ],
+ "tagOrder": ["Design"],
+ "assignments": [
+ {
+ "path": "/Applications/Figma.app",
+ "appName": "Figma",
+ "tags": ["Design"],
+ "confidence": 0.95,
+ "reason": "Design/prototyping tool"
+ }
+ ],
+ "unassigned": [],
+ "warnings": []
+}
+```
+
+The local app converts this draft into the existing `TagDatabase.Store` format:
+
+- `tags`: tag name to color
+- `tagOrder`: ordered category list
+- `appTags`: app path to tag-name array
+
+### Interaction Notes
+
+- Best entry point: Preferences > Tags, plus a small "AI Categorize" button in overlay edit mode.
+- Keep the first dialog small and discoverable, not a big chatbot.
+- Let users paste messy text; parse it into chips before upload.
+- Limit category names to short labels and warn on overly long names.
+- Never silently replace the user's existing layout. Show a before/after preview.
+- Add a one-click undo after applying.
+
+### Privacy And App Store Notes
+
+- Explicit upload confirmation is required.
+- The privacy copy should say exactly what is uploaded.
+- Add a local-only fallback: if the user declines upload, the app can still create empty tags from the typed list.
+- Update App Store privacy answers if the server feature ships.
+
+### Future Enhancements
+
+- Built-in local rules for common apps by bundle identifier.
+- User style presets: by work type, by life area, by frequency, by project, by app category.
+- Voice input using macOS dictation into the same text field, so the app does not need to handle audio processing in v1.
+- A later on-device model experiment if privacy becomes a key selling point, but this should not block the server-based MVP.
diff --git a/prd-importantback.md b/prd-importantback.md
new file mode 100644
index 0000000..e03f5d5
--- /dev/null
+++ b/prd-importantback.md
@@ -0,0 +1,85 @@
+# Important Design Notes
+
+This document records important product/design implementation ideas that should be easy to recover later.
+
+## View 2: Colorless Container
+
+The colorless container view groups apps by tag into independent frosted-material cards.
+
+Layout logic:
+
+- Use a masonry/waterfall layout.
+- Calculate how many columns fit in the available overlay width.
+- Estimate each tag card's height from its app count, current icon size, and card width.
+- Place each next tag group into the currently shortest column.
+- Each card uses a transparent background over `.ultraThinMaterial`, with only a subtle border by default.
+- When the user hovers or clicks a tag/card, the corresponding card is filled with that tag's color at low opacity, creating a temporary focus state.
+- Tag navigation hover/click scrolls to the matching card and also activates the same color fill.
+
+Design intent:
+
+- Keep the whole view visually light and non-color-dominant.
+- Use color only as an interaction/focus signal.
+- Let groups with different app counts occupy natural heights, so dense groups do not force empty space in neighboring groups.
+
+## View 4: Colorless Grid
+
+The colorless grid view also groups apps by tag into frosted-material cards, but aligns cards into row-based grid tracks instead of waterfall columns.
+
+Layout logic:
+
+- Choose a preferred number of grid tracks per row based on available width:
+ - 3 tracks on wide screens
+ - 2 tracks on medium screens
+ - 1 track on narrow screens
+- For each row, test span patterns:
+ - 3-track row: `1+1+1`, `1+2`, `2+1`, or `3`
+ - 2-track row: `1+1` or `2`
+ - 1-track row: `1`
+- Pick the lowest-cost pattern by estimating how many icon rows each card needs.
+- Cards in the same row share the same fixed icon-row height, so their bottoms align.
+- Inside each card, apps are manually split into rows and empty cells are filled with clear placeholders to preserve grid alignment.
+- Like colorless container view, the card is transparent over `.ultraThinMaterial` by default and only receives low-opacity tag color on hover/click focus.
+
+Design intent:
+
+- Preserve the clarity of grouped cards while making rows visually orderly.
+- Avoid the uneven bottom edges of masonry when the user wants a stricter grid feeling.
+- Keep the "colorless by default, colored only on focus" interaction model consistent with View 2.
+
+## View 3: Colored Container
+
+The colored container view uses the same masonry/waterfall layout as View 2, but each tag card is always tinted with its tag color.
+
+Interaction logic:
+
+- The card background is always filled with the tag color at low opacity over `.ultraThinMaterial`.
+- Hovering a card raises visual emphasis with shadow and a slight scale-up.
+- Unlike the colorless container view, clicking the card does not toggle a persistent fill state because the card is already colored.
+- Tag navigation hover/click still scrolls to the matching card, but the key visual distinction comes from the card's permanent tag color.
+- App drag/drop behavior remains the same: dropping an app onto a card moves or copies it into that tag group.
+
+Design intent:
+
+- Make tag grouping immediately visible without requiring hover.
+- Use color as a persistent category marker.
+- Keep the masonry layout's compactness while making group identity stronger than View 2.
+
+## View 5: Colored Grid
+
+The colored grid view uses the same row-aligned grid layout as View 4, but each tag card is always tinted with its tag color.
+
+Interaction logic:
+
+- Cards are arranged with the View 4 grid-row algorithm: adaptive track count, span pattern selection, and equal fixed icon-row height per row.
+- Each card background is always filled with the tag color at low opacity over `.ultraThinMaterial`.
+- Hovering a card adds shadow and a slight scale-up to show focus.
+- Clicking does not toggle a fill state because the colored background is permanent.
+- Empty cells are still inserted inside each card to keep icon rows aligned.
+- App drag/drop behavior remains the same as other container views.
+
+Design intent:
+
+- Combine the stronger category recognition of colored cards with the orderly rhythm of the grid layout.
+- Make dense app libraries easier to scan by preserving row alignment.
+- Use hover only for focus emphasis, not for revealing color.
--
Gitblit v1.9.3