Ariver
2026-05-19 57abfdc2ba880be1fae4a1a184531e3228d62dae
Prepare App Store release 7.3.5 build 744
9 files modified
1 files deleted
1 files added
533 ■■■■ changed files
AppStore_Submission.md 39 ●●●● patch | view | raw | blame | history
AppStore_TODO_5.7.8_583.md 55 ●●●●● patch | view | raw | blame | history
AppStore_TODO_7.3.5_744.md 100 ●●●●● patch | view | raw | blame | history
Apptag/AppGridItem.swift 13 ●●●●● patch | view | raw | blame | history
Apptag/ApptagApp.swift 25 ●●●●● patch | view | raw | blame | history
Apptag/ContentView.swift 125 ●●●●● patch | view | raw | blame | history
Apptag/PreferencesView.swift 140 ●●●●● patch | view | raw | blame | history
Apptag/SmartCategorization/SmartCategorizationDraft.swift 5 ●●●● patch | view | raw | blame | history
Apptag/SmartCategorization/SmartStartService.swift 24 ●●●● patch | view | raw | blame | history
Apptag/TagGroupView.swift 2 ●●●●● patch | view | raw | blame | history
CHANGELOG.md 5 ●●●●● patch | view | raw | blame | history
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` 生成正式签名包
AppStore_TODO_5.7.8_583.md
File was deleted
AppStore_TODO_7.3.5_744.md
New file
@@ -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`,方便审核反馈时快速对照。
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)
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 {
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 {
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
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] {
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
                        }
                    }
                }
            }
        }
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) }
                    )
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