From 57abfdc2ba880be1fae4a1a184531e3228d62dae Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Tue, 19 May 2026 01:20:21 +0800
Subject: [PATCH] Prepare App Store release 7.3.5 build 744
---
Apptag/TagGroupView.swift | 2
/dev/null | 55 ------
Apptag/ApptagApp.swift | 25 ++
AppStore_Submission.md | 39 ++--
Apptag/AppGridItem.swift | 13 -
Apptag/ContentView.swift | 125 +++++++++----
AppStore_TODO_7.3.5_744.md | 100 +++++++++++
Apptag/PreferencesView.swift | 140 ++++++++++++++-
Apptag/SmartCategorization/SmartStartService.swift | 24 +
Apptag/SmartCategorization/SmartCategorizationDraft.swift | 5
CHANGELOG.md | 5
11 files changed, 393 insertions(+), 140 deletions(-)
diff --git a/AppStore_Submission.md b/AppStore_Submission.md
index 5d9a815..6676582 100644
--- a/AppStore_Submission.md
+++ b/AppStore_Submission.md
@@ -1,6 +1,6 @@
# Apptag — Mac App Store 提审资料
-> 版本: 6.0.5 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-17
+> 版本: 7.3.5 | Build: 744 | Bundle ID: com.apptag.launcher | 更新日期: 2026-05-18
---
@@ -11,8 +11,8 @@
| **App 名称** | Apptag |
| **副标题** | Tag-Based App Launcher |
| **Bundle ID** | com.apptag.launcher |
-| **版本号** | 6.0.5 |
-| **Build 号** | 610 |
+| **版本号** | 7.3.5 |
+| **Build 号** | 744 |
| **SKU** | apptag-mac-001 |
| **主要类别** | Utilities (工具) |
| **次要类别** | Productivity (效率) |
@@ -175,7 +175,9 @@
## 7. App 图标
-已有自定义图标文件: `~/Projects/Apptag/icon-icns.icns`
+已有自定义图标文件:
+- `~/Projects/Apptag/icon-icns.icns`
+- `~/Projects/Apptag/Apptag/Assets.xcassets/AppIcon.appiconset/icon_1024_preview.png`
App Store 额外要求:
- **1024×1024 PNG** (用于 App Store 列表展示)
@@ -187,23 +189,21 @@
## 8. What's New (本次版本更新说明)
```
-Apptag 6.0.5
+Apptag 7.3.5
-- Added an Uncommon app marker for apps you are still learning, with optional hover notes
-- New apps can be marked as Uncommon automatically and are cleared after enough launches from TagLauncher
-- Added local note editing for Uncommon apps; notes stay in the exported JSON even if the marker is removed later
-- Added an Uncommon bubble toggle in the Data preferences tab, enabled by default
-- Reworked batch category editing into clear Add Tags and Remove Tags modes
-- Fixed batch editing so selected apps no longer inherit unrelated tags from each other
-- Added clear confirmation feedback showing the real number of tag links added or removed
-- Improved Remove Tags mode with red delete indicators and safer label truncation
-- Fixed edit-sidebar layout so long localized hints no longer stretch the page
-- Removed the edit-page center title and gave more room to the action hint and confirmation button
-- Improved shortcut hints in Language, Data, and About settings pages using symbol-only key graphics
-- Fixed a crash caused by toolbar hint layout constraints in the edit screen
+- Added Smart Start intelligent initial categorization with a built-in macOS app catalog, localized system tags, and default app notes
+- Added category scheme backup, restore, export, import, and automatic snapshots for safer tag management
+- Improved overlay performance with app scan caching, reduced full-view rebuilds, and smoother hover behavior
+- Improved drag-to-uncategorized handling with an in-overlay confirmation panel and better visual cleanup during drag operations
- Fixed Safari and other modern macOS system apps being missed when they live behind Cryptex system paths
-- Kept duplicate app icons out by resolving symlinks for scanning deduplication
-- Version updated to 6.0.5, Build updated to 610
+- Improved settings and edit controls, including more stable settings access from the overlay and better multilingual layout behavior
+- Improved menu bar item stability with a persistent status item identity so macOS and menu bar managers can recognize it consistently
+- Fixed localized default notes for Apple apps during first launch in non-Chinese languages
+- Fixed Uncategorized drag-and-drop confirmation so cancel/confirm no longer leaves icon drag state stuck
+- Improved tag-list reordering feedback by replacing persistent wiggle animation with clearer opacity-based drag state
+- Fixed the Smart Start apply confirmation in Settings so the warning stays above the Settings window with clearer readable controls
+- Fixed right-side tag navigation getting stuck in a dimmed drag visual state after a tag click
+- Version updated to 7.3.5, Build updated to 744
```
---
@@ -263,6 +263,7 @@
- [ ] 生成 1024×1024 App Store 图标 PNG
- [ ] 截取 6 张 Mac 截图 (3456×2234 PNG)
- [ ] 准备隐私政策 URL(GitHub Pages / 独立页面 / Notion)
+- [ ] 确认 `Apptag/Info.plist` 与构建产物均为 `7.3.5` / Build `744`
- [ ] 确认代码签名证书 (Mac App Distribution)
- [ ] 通过 TestFlight 测试 sandbox 下快捷键功能
- [ ] 使用 `APP_STORE=1 CODESIGN_IDENTITY="..." bash build.sh` 生成正式签名包
diff --git a/AppStore_TODO_5.7.8_583.md b/AppStore_TODO_5.7.8_583.md
deleted file mode 100644
index 1177bf2..0000000
--- a/AppStore_TODO_5.7.8_583.md
+++ /dev/null
@@ -1,55 +0,0 @@
-# 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/AppStore_TODO_7.3.5_744.md b/AppStore_TODO_7.3.5_744.md
new file mode 100644
index 0000000..b8ecf55
--- /dev/null
+++ b/AppStore_TODO_7.3.5_744.md
@@ -0,0 +1,100 @@
+# Apptag 7.3.5 (Build 744) App Store 提交 To-do List
+
+> 当前发布版本以源码目录中的 `Apptag/Info.plist` 为准;在发布包里对应 `Source/Apptag/Info.plist`:`CFBundleShortVersionString = 7.3.5`,`CFBundleVersion = 744`,`LSMinimumSystemVersion = 15.0`。
+> 本地已有构建产物 `build/TagLauncher.app` 与 `build/TagLauncher.dmg`,但正式提交仍建议在登录 Apple Developer 账号的开发者电脑上重新签名、归档、上传。
+
+## 0. 当前状态
+
+- 本地发布包已整理到 `/Users/ar/Projects/Apptag744Release`。
+- 本地 QA 已通过,详见 `QA/QA_Report_7.3.5_744.md`。
+- 发布资料版本漂移已清理:当前提审资料、清单、发布包均指向 `7.3.5` / Build `744`。
+- 本地构建产物为 ad-hoc 签名,仅用于 QA;正式提审仍需使用 Apple Distribution 证书重新签名并上传。
+- 下一步需要 Apple Developer 账号环境执行:正式签名、上传、TestFlight/App Store 签名包验证、提交审核。
+
+## A. 发布资料确认
+
+- 确认 App Store Connect 中本次版本为 `7.3.5`,Build 选择 `744`。
+- 使用 `AppStore_Submission.md` 填写描述、关键词、隐私说明、审核备注和 What's New。
+- 使用 `CHANGELOG.md` 顶部 `7.3.5` 记录作为版本变更来源。
+- 发布包目录为 `/Users/ar/Projects/Apptag744Release`,其中 `00todolist.md` 是正式操作清单。
+- 确认发布包中包含:
+ - `AppStore_Submission.md`
+ - `CHANGELOG.md`
+ - `00todolist.md`
+ - `Artifacts/TagLauncher.app`
+ - `Artifacts/TagLauncher.dmg`
+ - `Artifacts/AppStore_1024.png`
+ - `Artifacts/AppIcon.icns`
+ - `Source/` 源码与构建依赖
+ - `QA/QA_Report_7.3.5_744.md`
+
+## B. 正式构建与签名
+
+- 登录正确的 Apple Developer 账号。
+- 确认 Xcode、Command Line Tools、Transporter 或 Xcode Organizer 可用。
+- 确认证书包含 Mac App Distribution / Apple Distribution。
+- 确认 Bundle ID 是 `com.apptag.launcher`。
+- 使用 App Store sandbox entitlements:`Apptag/TagLauncher.entitlements`。
+- 在开发者电脑上进入发布包源码目录后运行:
+
+```bash
+cd /Users/ar/Projects/Apptag744Release/Source
+APP_STORE=1 CODESIGN_IDENTITY="Apple Distribution: Your Name (TEAMID)" bash build.sh
+```
+
+- 构建完成后确认 About 页显示 `版本 7.3.5(构建 744)`。
+- 确认 `build/TagLauncher.app/Contents/Info.plist` 中:
+ - `CFBundleShortVersionString = 7.3.5`
+ - `CFBundleVersion = 744`
+ - `LSMinimumSystemVersion = 15.0`
+
+## C. 本地冒烟测试
+
+- 退出旧版 TagLauncher,再启动新版。
+- 如果菜单栏图标不可见,检查 Thaw / Bartender / Ice / Hidden Bar / Dozer 等菜单栏管理工具是否隐藏了 TagLauncher。
+- 确认 `⌥⇧空格` 能呼出应用列表。
+- 确认主列表 Flat / Colored Grid / Container 模式均能显示。
+- 确认设置按钮和编辑按钮都能从浮层右上角打开。
+- 确认设置页语言切换正常。
+- 确认标签拖拽排序、右侧标签点击切换、App 拖入标签、拖到未分类确认面板正常。
+- 确认数据页导出 / 导入分类与布局正常。
+- 确认 Smart Start 初始分类方案可应用、确认提示不被设置窗口遮挡、恢复和导出。
+- 确认 App Store 沙盒构建中 Launch at Login 控件隐藏。
+- 在 TestFlight 中重点验证 sandbox 下全局快捷键 `Shift+Option+Space`。
+- 已完成本地关键 QA:全新用户、已有用户、Smart Start、语言切换、备注保护、拖到未分类、标签拖拽、右侧标签点击切换、设置页确认层级、菜单栏稳定性。
+
+## D. App Store Connect 填写
+
+- 创建或选择 Apptag `7.3.5` 版本。
+- App 名称:`Apptag`。
+- Bundle ID:`com.apptag.launcher`。
+- SKU:`apptag-mac-001`。
+- 类别:Utilities,次要类别 Productivity。
+- 最低系统版本:macOS `15.0`。
+- 复制 `AppStore_Submission.md` 第 2 节 App 描述。
+- 复制第 3 节关键词。
+- 隐私标签选择“不收集任何数据”。
+- 年龄分级按第 5 节填写,最终应为 `4+`。
+- What's New 使用第 8 节 `Apptag 7.3.5` 内容。
+- 审核备注使用第 9 节,说明 Carbon 全局快捷键不需要辅助功能权限,且无网络请求。
+
+## E. 截图与图标
+
+- 上传 1024×1024 App Store 图标 PNG,优先使用 `Apptag/Assets.xcassets/AppIcon.appiconset/icon_1024_preview.png`。
+- 准备至少 1 张、建议 6 张 Mac 截图。
+- 推荐截图场景:
+ - 全屏浮层 Flat 或 Colored Grid 模式
+ - Container 模式
+ - 标签编辑与拖拽排序
+ - 设置页通用选项
+ - 数据页分类方案管理
+ - 菜单栏入口与应用浮层工作流
+
+## F. 上传与提交审核
+
+- 使用 Xcode Organizer 或 Transporter 上传正式签名构建。
+- 上传后在 App Store Connect 选择 Build `744`。
+- 等待处理完成后,确认版本页显示 `7.3.5 (744)`。
+- 检查描述、隐私、截图、价格、年龄分级、审核备注均已填写。
+- 提交审核。
+- 提交后保留 `/Users/ar/Projects/Apptag744Release`,方便审核反馈时快速对照。
diff --git a/Apptag/AppGridItem.swift b/Apptag/AppGridItem.swift
index 22c204c..5a9a61b 100644
--- a/Apptag/AppGridItem.swift
+++ b/Apptag/AppGridItem.swift
@@ -14,10 +14,10 @@
var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
var bubbleDisabled: Bool = false
var itemID: String
+ var dragResetToken: Int = 0
@Binding var hoveredAppItemID: String?
let onSelect: () -> Void
- @State private var wiggle = false
@State private var interactionFrame: CGRect = .zero
@AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
@@ -96,15 +96,10 @@
onEditNote?(app, interactionFrame)
}
}
- .rotationEffect(.degrees(dragModeActive ? (wiggle ? 2.0 : -2.0) : 0))
- .animation(
- dragModeActive
- ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
- : .default,
- value: wiggle
- )
+ .opacity(dragModeActive ? 0.92 : 1)
+ .animation(.easeOut(duration: 0.08), value: dragModeActive)
+ .id(dragResetToken)
.onChange(of: dragModeActive) { _, active in
- wiggle = active
if active {
setHoverState(false)
onBubbleHover?(app, interactionFrame, false)
diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index 3737c9b..72a1665 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -586,6 +586,7 @@
}
overlayWindow?.orderOut(nil)
removeOverlayKeyMonitor()
+ NotificationCenter.default.post(name: .tagLauncherOverlayDidHide, object: nil)
if force {
overlayWindow = nil
}
@@ -836,15 +837,25 @@
final class DismissibleHostingView<Content: View>: NSHostingView<Content> {
private let onBackdropTap: () -> Void
+ private var suppressBackdropDismiss = false
+ private var modalInteractionObserver: NSObjectProtocol?
@MainActor required init(rootView: Content) {
self.onBackdropTap = {}
super.init(rootView: rootView)
+ observeModalInteractionChanges()
}
init(rootView: Content, onBackdropTap: @escaping () -> Void) {
self.onBackdropTap = onBackdropTap
super.init(rootView: rootView)
+ observeModalInteractionChanges()
+ }
+
+ deinit {
+ if let modalInteractionObserver {
+ NotificationCenter.default.removeObserver(modalInteractionObserver)
+ }
}
@available(*, unavailable)
@@ -857,6 +868,10 @@
return
}
if hit == self {
+ if suppressBackdropDismiss {
+ super.mouseDown(with: event)
+ return
+ }
onBackdropTap()
return
}
@@ -879,6 +894,16 @@
super.mouseDown(with: event)
}
+ private func observeModalInteractionChanges() {
+ modalInteractionObserver = NotificationCenter.default.addObserver(
+ forName: .tagLauncherModalInteractionChanged,
+ object: nil,
+ queue: .main
+ ) { [weak self] notification in
+ self?.suppressBackdropDismiss = (notification.userInfo?["active"] as? Bool) ?? false
+ }
+ }
+
private func findTextFieldContainer(in view: NSView) -> TextFieldContainer? {
if let container = view as? TextFieldContainer { return container }
for sub in view.subviews {
diff --git a/Apptag/ContentView.swift b/Apptag/ContentView.swift
index 16f587e..0b877a7 100644
--- a/Apptag/ContentView.swift
+++ b/Apptag/ContentView.swift
@@ -10,6 +10,8 @@
static let tagLauncherDataDidChange = Notification.Name("TagLauncherDataDidChange")
static let tagLauncherOpenPreferencesRequested = Notification.Name("TagLauncherOpenPreferencesRequested")
static let tagLauncherOverlayDidShow = Notification.Name("TagLauncherOverlayDidShow")
+ static let tagLauncherOverlayDidHide = Notification.Name("TagLauncherOverlayDidHide")
+ static let tagLauncherModalInteractionChanged = Notification.Name("TagLauncherModalInteractionChanged")
}
// MARK: - Edit Phase
@@ -199,8 +201,8 @@
let colorIndex: Int
var dragModeActive: Bool = false
var isDragging: Bool = false
+ var dragResetToken: Int = 0
let action: () -> Void
- @State private var wiggle = false
private var bgColor: Color {
Color(nsColor: TagColor.nsColor(for: colorIndex))
@@ -222,20 +224,14 @@
.background(RoundedRectangle(cornerRadius: 7).fill(bgColor))
.shadow(color: .black.opacity(isDragging ? 0.34 : 0.2), radius: isDragging ? 8 : 3, y: isDragging ? 4 : 1)
.scaleEffect(isDragging ? 1.05 : 1.0)
- .rotationEffect(.degrees(dragModeActive ? (wiggle ? 1.8 : -1.8) : 0))
- .animation(
- dragModeActive
- ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
- : .default,
- value: wiggle
- )
- .onChange(of: dragModeActive) { _, active in
- wiggle = active
- }
+ .opacity(dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0)
+ .animation(.easeOut(duration: 0.08), value: isDragging)
+ .animation(.easeOut(duration: 0.08), value: dragModeActive)
+ .id(dragResetToken)
.contentShape(RoundedRectangle(cornerRadius: 7))
.onTapGesture {
- if !dragModeActive { action() }
- }
+ action()
+ }
}
}
@@ -246,8 +242,8 @@
let colorIndex: Int
var dragModeActive: Bool = false
var isDragging: Bool = false
+ var dragResetToken: Int = 0
let action: () -> Void
- @State private var wiggle = false
private var bgColor: Color {
Color(nsColor: TagColor.nsColor(for: colorIndex))
@@ -265,20 +261,14 @@
.background(RoundedRectangle(cornerRadius: 6).fill(bgColor))
.shadow(color: .black.opacity(isDragging ? 0.34 : 0.2), radius: isDragging ? 8 : 3, y: isDragging ? 4 : 1)
.scaleEffect(isDragging ? 1.03 : 1.0)
- .rotationEffect(.degrees(dragModeActive ? (wiggle ? 1.6 : -1.6) : 0))
- .animation(
- dragModeActive
- ? .easeInOut(duration: 0.12).repeatForever(autoreverses: true)
- : .default,
- value: wiggle
- )
- .onChange(of: dragModeActive) { _, active in
- wiggle = active
- }
+ .opacity(dragModeActive ? (isDragging ? 1.0 : 0.62) : 1.0)
+ .animation(.easeOut(duration: 0.08), value: isDragging)
+ .animation(.easeOut(duration: 0.08), value: dragModeActive)
+ .id(dragResetToken)
.contentShape(RoundedRectangle(cornerRadius: 6))
.onTapGesture {
- if !dragModeActive { action() }
- }
+ action()
+ }
}
}
@@ -364,7 +354,9 @@
@State private var tagReorderFrames: [String: CGRect] = [:]
@State private var tagNavDragModeActive = false
@State private var tagNavDragItem: String? = nil
+ @State private var tagNavDragResetToken = 0
@State private var tagNavReorderFrames: [String: CGRect] = [:]
+ @State private var tagNavReorderDidMove = false
@State private var hoveredContainer: String? = nil // colored container lift
// Fixed interaction for "Colorless Container": hover fills persistently; click clears.
@State private var filledColorlessContainer: String? = nil
@@ -381,6 +373,7 @@
@State private var hoveredBubble: AppBubbleContext? = nil
@State private var editingBubble: AppBubbleContext? = nil
@State private var pendingUncategorizedDrop: PendingUncategorizedDrop? = nil
+ @State private var appDragResetToken = 0
@State private var bubbleDraftNote = ""
@FocusState private var bubbleNoteFocused: Bool
@@ -488,8 +481,12 @@
refreshApps()
}
.onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidShow)) { _ in
+ resetTransientDragState()
refreshNotchHeight()
refreshApps()
+ }
+ .onReceive(NotificationCenter.default.publisher(for: .tagLauncherOverlayDidHide)) { _ in
+ resetTransientDragState()
}
.onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in
guard allApps.isEmpty, !refreshInProgress else { return }
@@ -520,6 +517,20 @@
if editingBubble != nil && !focused {
commitBubbleNote()
}
+ }
+ .onChange(of: pendingUncategorizedDrop != nil) { _, active in
+ NotificationCenter.default.post(
+ name: .tagLauncherModalInteractionChanged,
+ object: nil,
+ userInfo: ["active": active]
+ )
+ }
+ .onDisappear {
+ NotificationCenter.default.post(
+ name: .tagLauncherModalInteractionChanged,
+ object: nil,
+ userInfo: ["active": false]
+ )
}
}
@@ -613,11 +624,9 @@
TagPill(name: tag.name, colorIndex: tag.colorIndex,
dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
isDragging: tagNavDragItem == tag.name,
+ dragResetToken: tagNavDragResetToken,
action: {
- if isColorlessContainerMode {
- toggleColorlessFill(tag.id)
- }
- scrollTo(tag.id)
+ activateTagNavigation(tag.id)
})
.background(tagNavFrameReader(for: tag.name))
.zIndex(tagNavDragItem == tag.name ? 1 : 0)
@@ -657,11 +666,9 @@
SideTagPill(name: tag.name, colorIndex: tag.colorIndex,
dragModeActive: tagNavDragModeActive && canReorderTag(tag.name),
isDragging: tagNavDragItem == tag.name,
+ dragResetToken: tagNavDragResetToken,
action: {
- if isColorlessContainerMode {
- toggleColorlessFill(tag.id)
- }
- scrollTo(tag.id)
+ activateTagNavigation(tag.id)
})
.background(tagNavFrameReader(for: tag.name))
.zIndex(tagNavDragItem == tag.name ? 1 : 0)
@@ -840,6 +847,7 @@
onBubbleHover: handleBubbleHover,
onEditNote: beginEditingBubbleNote,
bubbleDisabled: appBubbleDisabled,
+ dragResetToken: appDragResetToken,
hoveredAppItemID: $hoveredAppItemID,
onDropApp: { path, source, copy in
dropApp(path: path, sourceTag: source, targetTag: group.name, copy: copy)
@@ -945,6 +953,7 @@
onEditNote: beginEditingBubbleNote,
bubbleDisabled: appBubbleDisabled,
itemID: "\(group.name)|\(app.path.path)",
+ dragResetToken: appDragResetToken,
hoveredAppItemID: $hoveredAppItemID,
onSelect: { openApp(app) }
)
@@ -1184,6 +1193,7 @@
onEditNote: beginEditingBubbleNote,
bubbleDisabled: appBubbleDisabled,
itemID: "\(group.name)|\(app.path.path)",
+ dragResetToken: appDragResetToken,
hoveredAppItemID: $hoveredAppItemID,
onSelect: { openApp(app) }
)
@@ -1985,6 +1995,14 @@
withAnimation(.easeInOut(duration: 0.25)) { scrollProxy?.scrollTo(id, anchor: .top) }
}
+ private func activateTagNavigation(_ id: String) {
+ cancelTagNavReorderVisualState()
+ if isColorlessContainerMode {
+ toggleColorlessFill(id)
+ }
+ scrollTo(id)
+ }
+
private func fillColorlessContainer(_ id: String) {
guard isColorlessContainerMode else { return }
guard filledColorlessContainer != id else { return }
@@ -2012,8 +2030,8 @@
}
private func tagNavReorderGesture(for tagName: String) -> some Gesture {
- LongPressGesture(minimumDuration: 0.5)
- .sequenced(before: DragGesture(minimumDistance: 3, coordinateSpace: .named("tagNavReorder")))
+ LongPressGesture(minimumDuration: 0.35)
+ .sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .named("tagNavReorder")))
.onChanged { value in
guard canReorderTag(tagName) else { return }
switch value {
@@ -2039,16 +2057,27 @@
guard canReorderTag(tagName) else { return }
if tagNavDragItem == nil {
tagNavDragItem = tagName
+ tagNavReorderDidMove = false
}
tagNavDragModeActive = true
}
private func endTagNavReorder() {
- if tagNavDragModeActive {
+ if tagNavDragModeActive && tagNavReorderDidMove {
TagEditor.reorderTags(draggedTagNames)
}
tagNavDragModeActive = false
tagNavDragItem = nil
+ tagNavReorderDidMove = false
+ tagNavDragResetToken &+= 1
+ }
+
+ private func cancelTagNavReorderVisualState() {
+ guard tagNavDragModeActive || tagNavDragItem != nil else { return }
+ tagNavDragModeActive = false
+ tagNavDragItem = nil
+ tagNavReorderDidMove = false
+ tagNavDragResetToken &+= 1
}
private func reorderTagNavItem(at location: CGPoint) {
@@ -2059,6 +2088,7 @@
let toIndex = draggedTagNames.firstIndex(of: targetName)
else { return }
+ tagNavReorderDidMove = true
withAnimation(.spring(response: 0.22, dampingFraction: 0.82)) {
let destination = toIndex > fromIndex ? toIndex + 1 : toIndex
draggedTagNames.move(fromOffsets: IndexSet(integer: fromIndex), toOffset: destination)
@@ -2093,7 +2123,7 @@
}
private func dropApp(path: String, sourceTag: String, targetTag: String, copy: Bool) {
- appDragModeActive = false
+ resetTransientDragState(keepingPendingUncategorizedDrop: true)
if isUncategorizedDropTarget(targetTag) {
confirmAndMoveAppToUncategorized(path: path)
@@ -2152,6 +2182,7 @@
}
private func dismissUncategorizedDropConfirm() {
+ resetTransientDragState(keepingPendingUncategorizedDrop: true)
withAnimation(.easeOut(duration: 0.18)) {
pendingUncategorizedDrop = nil
}
@@ -2161,6 +2192,7 @@
guard let pendingDrop = pendingUncategorizedDrop else { return }
let path = pendingDrop.app.path.path
let tags = pendingDrop.app.tags
+ resetTransientDragState(keepingPendingUncategorizedDrop: true)
withAnimation(.easeOut(duration: 0.16)) {
pendingUncategorizedDrop = nil
}
@@ -2230,6 +2262,23 @@
}
}
+ private func resetTransientDragState(keepingPendingUncategorizedDrop: Bool = false) {
+ AppDragCoordinator.shared.cancelDrag()
+ appDragModeActive = false
+ appDragResetToken &+= 1
+ tagNavDragModeActive = false
+ tagNavDragItem = nil
+ tagNavReorderDidMove = false
+ tagNavDragResetToken &+= 1
+ dragItem = nil
+ hoveredAppItemID = nil
+ hoveredContainer = nil
+ clearAppBubbleState()
+ if !keepingPendingUncategorizedDrop {
+ pendingUncategorizedDrop = nil
+ }
+ }
+
func refreshApps(forceLayoutRefresh: Bool = false) {
guard !refreshInProgress else {
if forceLayoutRefresh {
diff --git a/Apptag/PreferencesView.swift b/Apptag/PreferencesView.swift
index 88c00a4..d0a1843 100644
--- a/Apptag/PreferencesView.swift
+++ b/Apptag/PreferencesView.swift
@@ -30,6 +30,7 @@
@State private var tagColors: [String: Int] = [:]
@State private var categoryScheme = TagDatabase.CategorySchemeState()
@State private var isApplyingSystemScheme = false
+ @State private var showApplySystemSchemeConfirmation = false
private func scanApps() {
DispatchQueue.global(qos: .userInitiated).async {
@@ -135,7 +136,15 @@
}
private func applySystemInitialScheme() {
- guard confirmApplySystemInitialScheme() else { return }
+ guard !isApplyingSystemScheme else { return }
+ withAnimation(.easeOut(duration: 0.16)) {
+ showApplySystemSchemeConfirmation = true
+ }
+ }
+
+ private func performApplySystemInitialScheme() {
+ guard !isApplyingSystemScheme else { return }
+ showApplySystemSchemeConfirmation = false
TagDatabase.flushPendingCategorySchemeBackupBatch()
isApplyingSystemScheme = true
@@ -166,16 +175,6 @@
}
}
}
- }
-
- private func confirmApplySystemInitialScheme() -> Bool {
- let alert = NSAlert()
- alert.messageText = tr("settings.applySystemSchemeWarningTitle")
- alert.informativeText = tr("settings.applySystemSchemeWarningMessage")
- alert.alertStyle = .warning
- alert.addButton(withTitle: tr("settings.confirmApplyScheme"))
- alert.addButton(withTitle: tr("settings.cancel"))
- return alert.runModal() == .alertFirstButtonReturn
}
private func formattedSystemSchemeAppliedMessage(_ summary: SmartStartSummary) -> String {
@@ -697,6 +696,23 @@
}
.transition(.opacity)
}
+
+ if showApplySystemSchemeConfirmation {
+ ApplySystemSchemeConfirmationView(
+ title: tr("settings.applySystemSchemeWarningTitle"),
+ message: tr("settings.applySystemSchemeWarningMessage"),
+ confirmTitle: tr("settings.confirmApplyScheme"),
+ cancelTitle: tr("settings.cancel"),
+ onConfirm: performApplySystemInitialScheme,
+ onCancel: {
+ withAnimation(.easeOut(duration: 0.14)) {
+ showApplySystemSchemeConfirmation = false
+ }
+ }
+ )
+ .zIndex(3)
+ .transition(.opacity)
+ }
}
.frame(width: settingsWindowWidth, height: 460)
.onAppear {
@@ -714,6 +730,108 @@
}
}
+private struct ApplySystemSchemeConfirmationView: View {
+ let title: String
+ let message: String
+ let confirmTitle: String
+ let cancelTitle: String
+ let onConfirm: () -> Void
+ let onCancel: () -> Void
+
+ var body: some View {
+ ZStack {
+ Color.black.opacity(0.20)
+ .contentShape(Rectangle())
+ .onTapGesture { }
+
+ VStack(alignment: .leading, spacing: 20) {
+ HStack(alignment: .top, spacing: 12) {
+ ZStack {
+ Circle()
+ .fill(Color.orange.opacity(0.16))
+ .frame(width: 38, height: 38)
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 19, weight: .semibold))
+ .foregroundStyle(Color.orange)
+ }
+ .frame(width: 40, height: 40)
+
+ VStack(alignment: .leading, spacing: 8) {
+ Text(title)
+ .font(.system(size: 16, weight: .semibold))
+ .foregroundStyle(.primary)
+ .fixedSize(horizontal: false, vertical: true)
+ Text(message)
+ .font(.system(size: 13))
+ .foregroundStyle(Color.primary.opacity(0.78))
+ .lineSpacing(2)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ }
+
+ HStack(spacing: 10) {
+ Spacer()
+ ConfirmationActionButton(
+ title: cancelTitle,
+ prominent: false,
+ action: onCancel
+ )
+ .keyboardShortcut(.cancelAction)
+ ConfirmationActionButton(
+ title: confirmTitle,
+ prominent: true,
+ action: onConfirm
+ )
+ .keyboardShortcut(.defaultAction)
+ }
+ }
+ .padding(22)
+ .frame(width: 440)
+ .background(
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .fill(Color(nsColor: .windowBackgroundColor))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 8, style: .continuous)
+ .stroke(Color.primary.opacity(0.16), lineWidth: 1)
+ )
+ .shadow(color: .black.opacity(0.24), radius: 20, x: 0, y: 10)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+}
+
+private struct ConfirmationActionButton: View {
+ let title: String
+ let prominent: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(prominent ? Color.white : Color.primary)
+ .lineLimit(1)
+ .minimumScaleFactor(0.86)
+ .frame(minWidth: 82, minHeight: 32)
+ .padding(.horizontal, 10)
+ .background(
+ RoundedRectangle(cornerRadius: 7, style: .continuous)
+ .fill(prominent ? Color.accentColor : Color(nsColor: .controlBackgroundColor))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: 7, style: .continuous)
+ .stroke(
+ prominent ? Color.accentColor.opacity(0.40) : Color.primary.opacity(0.16),
+ lineWidth: 1
+ )
+ )
+ }
+ .buttonStyle(.plain)
+ .contentShape(RoundedRectangle(cornerRadius: 7, style: .continuous))
+ }
+}
+
private struct DisplayModeOptionButton: View {
let mode: String
let title: String
diff --git a/Apptag/SmartCategorization/SmartCategorizationDraft.swift b/Apptag/SmartCategorization/SmartCategorizationDraft.swift
index a3b9715..552466e 100644
--- a/Apptag/SmartCategorization/SmartCategorizationDraft.swift
+++ b/Apptag/SmartCategorization/SmartCategorizationDraft.swift
@@ -54,6 +54,7 @@
let reason: String
let provenance: [String]
let defaultNote: String?
+ let defaultNoteCandidates: [String]
var id: String {
if let bundleIdentifier, !bundleIdentifier.isEmpty {
@@ -78,7 +79,8 @@
source: SmartCategorizationAssignmentSource,
reason: String,
provenance: [String] = [],
- defaultNote: String? = nil
+ defaultNote: String? = nil,
+ defaultNoteCandidates: [String] = []
) {
self.appName = appName
self.bundleIdentifier = bundleIdentifier
@@ -89,6 +91,7 @@
self.reason = reason
self.provenance = provenance
self.defaultNote = defaultNote
+ self.defaultNoteCandidates = defaultNoteCandidates
}
private static func uniqueOrdered(_ ids: [SmartCategoryID]) -> [SmartCategoryID] {
diff --git a/Apptag/SmartCategorization/SmartStartService.swift b/Apptag/SmartCategorization/SmartStartService.swift
index 97016f4..220373a 100644
--- a/Apptag/SmartCategorization/SmartStartService.swift
+++ b/Apptag/SmartCategorization/SmartStartService.swift
@@ -261,7 +261,8 @@
source: .localCatalog,
reason: "Matched Smart Start catalog entry: \(matched.name)",
provenance: matched.sourceEvidence,
- defaultNote: matched.localizedNote
+ defaultNote: matched.localizedNote,
+ defaultNoteCandidates: matched.notes?.values.map { $0 } ?? []
))
}
@@ -348,12 +349,21 @@
}
if let defaultNote = assignment.defaultNote?.trimmingCharacters(in: .whitespacesAndNewlines),
- !defaultNote.isEmpty,
- store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty != false {
- store.appNotes[path] = String(defaultNote.prefix(TagDatabase.maxAppNoteLength))
- uncommonPaths.insert(path)
- if store.uncommonSources[path] == nil {
- store.uncommonSources[path] = .auto
+ !defaultNote.isEmpty {
+ let currentNote = store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines)
+ let knownDefaultNotes = Set(assignment.defaultNoteCandidates.compactMap(normalizedNote))
+ let normalizedCurrentNote = normalizedNote(currentNote)
+ let shouldSeedNote = currentNote?.isEmpty != false
+ || normalizedCurrentNote.map { knownDefaultNotes.contains($0) } == true
+
+ if shouldSeedNote {
+ store.appNotes[path] = String(defaultNote.prefix(TagDatabase.maxAppNoteLength))
+ if currentNote?.isEmpty != false {
+ uncommonPaths.insert(path)
+ if store.uncommonSources[path] == nil {
+ store.uncommonSources[path] = .auto
+ }
+ }
}
}
}
diff --git a/Apptag/TagGroupView.swift b/Apptag/TagGroupView.swift
index bea1637..02075e9 100644
--- a/Apptag/TagGroupView.swift
+++ b/Apptag/TagGroupView.swift
@@ -14,6 +14,7 @@
var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil
var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
var bubbleDisabled: Bool = false
+ var dragResetToken: Int = 0
@Binding var hoveredAppItemID: String?
var onDropApp: ((String, String, Bool) -> Void)? = nil
@@ -61,6 +62,7 @@
onEditNote: onEditNote,
bubbleDisabled: bubbleDisabled,
itemID: "\(group.name)|\(app.path.path)",
+ dragResetToken: dragResetToken,
hoveredAppItemID: $hoveredAppItemID,
onSelect: { onSelectApp(app) }
)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index db40728..40e1e5d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,11 @@
- 为菜单栏图标设置稳定 `NSStatusItem.autosaveName`,让 macOS 与 Thaw 更容易识别为同一个菜单栏项
- 菜单栏刷新时复用已有 `NSStatusItem`,避免语言刷新等场景反复删除重建导致位置记忆失效
- 为菜单栏按钮补充稳定 identifier 与 accessibility 标识,提升第三方菜单栏管理工具识别稳定性
+- 修复全新用户使用英文等非中文界面时,部分 Apple App 默认用途备注仍被旧路径种成中文的问题
+- 修复拖动 App 到“未分类”确认/取消后,浮层可能关闭且图标持续抖动、拖拽状态残留的问题
+- 优化标签列表拖拽排序反馈:移除标签无限抖动,改为拖拽时其他标签降透明度,并缩短长按响应时间
+- 修复设置页“应用系统智能化初始分类”确认提示可能跑到设置窗口背后、被遮挡的问题,并优化为更清晰的实底确认框
+- 修复右侧标签列表点击后可能停留在拖拽视觉状态,导致后续标签无法正常点击切换的问题
- 版本号更新为 `7.3.5`,Build 更新为 `744`
## [7.3.4] — 2026-05-18
--
Gitblit v1.9.3