| AppStore_Submission.md | ●●●●● patch | view | raw | blame | history | |
| AppStore_TODO_5.7.8_583.md | ●●●●● patch | view | raw | blame | history | |
| AppStore_TODO_7.3.5_744.md | ●●●●● patch | view | raw | blame | history | |
| Apptag/AppGridItem.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/ApptagApp.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/ContentView.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/PreferencesView.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/SmartCategorization/SmartCategorizationDraft.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/SmartCategorization/SmartStartService.swift | ●●●●● patch | view | raw | blame | history | |
| Apptag/TagGroupView.swift | ●●●●● patch | view | raw | blame | history | |
| CHANGELOG.md | ●●●●● 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,19 +224,13 @@ .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,19 +261,13 @@ .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,15 +349,24 @@ } if let defaultNote = assignment.defaultNote?.trimmingCharacters(in: .whitespacesAndNewlines), !defaultNote.isEmpty, store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty != false { !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 } } } } } store.uncommonAppPaths = uncommonPaths.sorted() store.smartStart.catalogVersion = catalogVersion 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