| docs/7.90/03-产品PRD.md | ●●●●● patch | view | raw | blame | history | |
| docs/7.90/04-QA测试用例集.md | ●●●●● patch | view | raw | blame | history | |
| docs/7.90/05-技术设计方案.md | ●●●●● patch | view | raw | blame | history | |
| docs/7.90/README.md | ●●●●● patch | view | raw | blame | history |
docs/7.90/03-产品PRD.md
New file @@ -0,0 +1,125 @@ # 7.9.0 产品 PRD:容器内 App 手动排序 ## 1. 背景 TagLauncher 当前支持按标签/容器展示 App,也支持拖动 App 改变标签归属。但在同一个容器内,App 的显示顺序主要来自扫描后的默认名称排序,用户无法把高频 App 固定到自己习惯的位置。 对重度用户来说,容器不仅是分类,也是工作流入口。用户希望像整理桌面图标一样,在每个容器中把 App 摆成自己的顺序,并且这个顺序能随“分类与布局”一起保存、导入、恢复。 ## 2. 目标 - 用户可以在同一个容器/分组内拖动 App,改变该容器内显示顺序。 - 自定义顺序在刷新、重启、语言切换、导入导出、恢复上一方案、SmartStart 回滚后保持一致。 - 不破坏现有跨容器拖动改标签、Option 复制、拖到空白移除标签、Apple 内置组保护等既有行为。 - 将该需求作为 `7.9.0` 大版本需求管理。 ## 3. 用户价值 - 高频 App 可以放到容器最前面,减少寻找成本。 - 每个标签/容器可以形成独立工作流顺序。 - 多标签 App 在不同容器里可以有不同位置,符合不同使用语境。 - 导出“分类与布局”时真正包含布局顺序,便于迁移和恢复。 ## 4. 适用用户 - 安装 App 数量较多、依赖 TagLauncher 做日常启动入口的用户。 - 已经维护多个标签/容器,并希望在容器内进一步精细整理的用户。 - 需要在多台 Mac 或重装后恢复分类与布局的用户。 ## 5. 范围 ### 5.1 版本 7.9.0 必做 - 同一容器内拖动 App 改变顺序。 - 每个容器独立保存顺序。 - 多标签 App 在不同容器中的顺序互不影响。 - 新 App 或未记录 App 在自定义顺序之后按默认规则显示。 - 删除/卸载 App 后不显示空位、不崩溃。 - 顺序纳入分类与布局数据,可导出、导入、恢复。 - 旧用户数据无顺序字段时保持原有默认排序。 ### 5.2 版本 7.9.0 不做 - 不做跨容器插入位置排序。 - 不改变跨容器拖动的标签归属语义。 - 不改变 Quick Search 结果排序。 - 不提供“按名称重新排序”“重置单个容器顺序”等高级管理入口。 - 不做跨机器 bundle id/name fallback 的完整稳定匹配;第一版以 path 为 App 顺序身份。 ## 6. 核心交互 ### 6.1 同容器排序 用户在 App Grid 普通展示态中,长按并拖动某个 App 到同一容器中的另一个位置。 期望: - 拖动过程中显示清晰的插入位置提示。 - 松手后该 App 移动到目标位置。 - 不改变该 App 的标签归属。 - 不触发 App 启动。 ### 6.2 跨容器拖动 用户把 App 拖到其他容器/标签。 期望: - 沿用现有移动/复制标签逻辑。 - 按住 Option 时继续执行复制标签语义。 - 不把跨容器拖动解释为排序。 ### 6.3 拖到空白区域 用户把 App 拖到容器外空白区域。 期望: - 继续沿用现有移除来源标签或归为未分类确认逻辑。 - 不因为新增排序功能改变提醒、确认或“不再提醒”逻辑。 ### 6.4 特殊容器 - “未分类”容器:第一版允许读取排序;是否开放拖动排序按实现风险在开发阶段确认。 - “Mac 自带 / Apple 内置”容器:保持现有保护逻辑,不允许通过拖动改变系统标签归属;排序是否开放需单独评估,默认不作为第一优先级。 ## 7. 数据行为 - 自定义顺序属于“分类与布局方案”。 - 导出分类与布局时必须包含顺序。 - 导入分类与布局时必须恢复顺序。 - 恢复上一方案时必须恢复顺序。 - SmartStart 应用新方案前的备份必须包含顺序。 - “重置为未分类”应清理旧容器顺序,避免旧布局在未来意外复活。 ## 8. 兼容性要求 - macOS 14 起可用。 - 旧 `tags.json` 无新增字段时可正常读取。 - 旧版本 App 读取新 JSON 时不应破坏已知字段;新版本 App 重新读取后仍应尽量保留顺序。 - 语言切换不影响顺序,因为顺序 key 不能依赖本地化显示名。 ## 9. 验收标准 - 用户可在同容器内完成拖动排序。 - 排序结果立即显示。 - 关闭并重新打开 App Grid 后顺序保持。 - 重启 App 后顺序保持。 - 切换语言后顺序保持。 - 导出再导入后顺序保持。 - 恢复上一方案后顺序恢复。 - 跨容器拖动、Option 复制、拖到空白、Apple 内置保护保持现有行为。 - 旧数据用户升级后默认顺序不变。 ## 10. 发布要求 - 版本号从 `7.9.0` 开始。 - `CHANGELOG.md` 必须记录数据模型、交互范围和兼容性说明。 - 发布前必须有 QA 报告,覆盖数据层、UI 手工拖拽、macOS 14、导入导出、恢复、SmartStart 回归。 ## 11. 未决问题 - “未分类”容器是否开放拖动排序。 - “Mac 自带 / Apple 内置”容器是否开放仅显示顺序排序。 - 是否需要在后续版本提供“重置当前容器顺序”入口。 - 是否需要在后续版本支持跨机器导入时基于 bundle id/name 匹配顺序。 docs/7.90/04-QA测试用例集.md
New file @@ -0,0 +1,123 @@ # 7.9.0 QA 测试用例集:容器内 App 手动排序 ## 1. 测试目标 验证用户可以在同一容器内拖动 App 改变显示顺序,并确保顺序在刷新、重启、语言切换、导入导出、恢复、SmartStart 等路径中保持一致,同时不破坏现有跨容器拖动改标签行为。 ## 2. 测试范围 - 数据模型兼容性。 - 同容器拖动排序。 - 旧拖拽行为回归。 - 多显示模式和标签位置。 - 导入导出、恢复、重置、SmartStart。 - macOS 14 兼容性。 ## 3. 测试环境 - macOS 14 Apple Silicon 真机。 - 当前开发机最新 macOS。 - 干净用户数据一份。 - 旧版 `tags.json` fixture 一份。 - 含多标签 App、未分类 App、Apple 内置 App、已卸载 path 的测试数据一份。 ## 4. 自动化用例 | ID | 优先级 | 类型 | 用例 | 期望结果 | | --- | --- | --- | --- | --- | | DATA-001 | P0 | 自动 | 旧 `tags.json` 无 `containerAppOrder` 字段时解码 | 加载成功,默认顺序与旧版本一致 | | DATA-002 | P0 | 自动 | 新 `tags.json` 含 `containerAppOrder` 字段时解码 | 加载成功,顺序字段保留 | | DATA-003 | P0 | 自动 | 导出后再导入含顺序的分类与布局 | 导入后每个容器顺序一致 | | DATA-004 | P0 | 自动 | `CategorySchemeFingerprint` 包含顺序字段 | 只改变顺序也能触发上一方案快照 | | DATA-005 | P0 | 自动 | 恢复上一方案 | 标签、App 归属、容器内顺序都恢复 | | DATA-006 | P0 | 自动 | 重置为未分类 | 清空旧普通标签归属,并按既定策略清理旧容器顺序 | | DATA-007 | P0 | 自动 | SmartStart replace | 新方案顺序可预测,不复用旧容器顺序 | | DATA-008 | P1 | 自动 | tag rename/delete/relocalize | 顺序 key 正确迁移或清理 | | DATA-009 | P1 | 自动 | 卸载 App path 出现在顺序字段 | 不显示空位,不崩溃,必要时懒清理 | | DATA-010 | P1 | 自动 | 新 App 不在顺序字段内 | 已排序 App 保持,新增 App 追加并按默认名称排序 | ## 5. 手工功能用例 | ID | 优先级 | 用例 | 步骤 | 期望结果 | | --- | --- | --- | --- | --- | | UI-001 | P0 | 同容器拖动到最前 | 在某容器中把第 3 个 App 拖到第 1 个位置 | App 顺序立即改变,不启动 App | | UI-002 | P0 | 同容器拖动到中间 | 把最后一个 App 拖到中间位置 | 插入位置正确,顺序保存 | | UI-003 | P0 | 跨行/跨列拖动 | 在网格容器中跨行拖动 App | 目标位置正确,无错位 | | UI-004 | P0 | 拖回原位 | 长按拖动后放回原位置 | 不写入无意义变更,不闪烁 | | UI-005 | P0 | 松手未移动 | 长按 App 后直接松手 | 不启动 App,不改变顺序 | | UI-006 | P0 | 多标签 App 独立顺序 | App 同时在 A/B 容器,重排 A | A 顺序改变,B 顺序不变 | | UI-007 | P0 | 重开 App Grid | 排序后关闭再打开 App Grid | 顺序保持 | | UI-008 | P0 | 重启 App | 排序后退出并重启 TagLauncher | 顺序保持 | | UI-009 | P1 | 语言切换 | 排序后切换中英文/日文 | 顺序保持,显示名变化不影响位置 | | UI-010 | P1 | 新安装 App | 排序后新增一个 App 并刷新 | 旧顺序保持,新 App 按默认规则追加 | ## 6. 旧拖拽行为回归 | ID | 优先级 | 用例 | 步骤 | 期望结果 | | --- | --- | --- | --- | --- | | DRAG-001 | P0 | 跨容器移动标签 | 从 A 容器拖 App 到 B 容器 | App 获得/移动到 B 标签,旧逻辑保持 | | DRAG-002 | P0 | Option 复制标签 | 按住 Option 从 A 拖到 B | App 同时保留 A/B 标签 | | DRAG-003 | P0 | 拖到空白移除来源标签 | 从容器拖到空白区域 | 继续出现现有确认逻辑 | | DRAG-004 | P0 | 拖到未分类 | 拖到未分类目标 | 继续走未分类确认与记忆逻辑 | | DRAG-005 | P0 | 拖到 Apple 内置组 | 拖到 Mac 自带 / Apple 内置组 | 继续被保护,不改变标签 | | DRAG-006 | P1 | 标签栏拖拽排序 | 拖动顶部/侧边标签排序 | 不受 App 排序拖拽影响 | ## 7. 显示模式矩阵 | ID | 优先级 | displayMode | 验收 | | --- | --- | --- | --- | | MODE-001 | P0 | `flat` | 顺序读取一致;是否允许拖动按产品最终边界验收 | | MODE-002 | P0 | `container` | 可同容器排序 | | MODE-003 | P0 | `coloredContainer` | 可同容器排序,颜色填充不遮挡插入提示 | | MODE-004 | P0 | `gridContainer` | 可同容器排序,跨行插入正确 | | MODE-005 | P0 | `coloredGridContainer` | 可同容器排序,跨行插入正确 | ## 8. 标签位置矩阵 | ID | 优先级 | 标签位置 | 验收 | | --- | --- | --- | --- | | NAV-001 | P1 | 左侧 | App 排序和标签导航互不冲突 | | NAV-002 | P1 | 右侧 | App 排序和标签导航互不冲突 | | NAV-003 | P1 | 顶部 | App 排序和标签导航互不冲突 | ## 9. 尺寸与显示矩阵 | ID | 优先级 | 条件 | 验收 | | --- | --- | --- | --- | | VIEW-001 | P1 | hide app names 开 | 拖动排序时不因名称隐藏误触发气泡 | | VIEW-002 | P1 | hide app names 关 | 名称不遮挡插入提示 | | VIEW-003 | P1 | icon size 40 | 小图标可拖动,命中正确 | | VIEW-004 | P1 | icon size 64 | 默认尺寸可拖动 | | VIEW-005 | P1 | icon size 80 | 大图标可拖动,布局不重叠 | ## 10. 异常与取消 | ID | 优先级 | 用例 | 期望结果 | | --- | --- | --- | --- | | EDGE-001 | P0 | 拖出窗口后松手 | 状态清理干净,不残留拖拽影子 | | EDGE-002 | P1 | 拖动中按 Esc | 取消拖动,顺序不变 | | EDGE-003 | P1 | 拖动中滚动 | 不出现错位插入;若暂不支持滚动拖拽,边界表现明确 | | EDGE-004 | P1 | 拖动中切换 Space/窗口 | 不崩溃,不残留 hover/bubble 状态 | | EDGE-005 | P1 | 拖动中打开 Quick Search/Settings | 拖动状态清理,窗口行为正常 | ## 11. 现有 QA 脚本 实现完成后至少运行: ```bash cd /Users/ar/Projects/Taglauncher/src bash Scripts/macos14_availability_typecheck_qa.sh bash Scripts/macos14_build_metadata_qa.sh bash Scripts/quick_search_app_name_qa.sh bash Scripts/quick_search_system_app_qa.sh bash Scripts/smartstart_catalog_resource_qa.sh bash Scripts/window_logic_qa.sh ``` ## 12. 发布阻断条件 - 顺序在刷新、重启、语言切换、导入导出、恢复上一方案任一路径丢失。 - 同容器排序破坏跨容器移动、Option 复制、拖空白移除、Apple 内置保护。 - 旧数据无法加载或升级后默认顺序改变。 - macOS 14 typecheck/build metadata/window logic 任一失败。 - 手工发现拖拽残影、误启动 App、窗口关闭、UI 重叠或插入位置不可理解。 docs/7.90/05-技术设计方案.md
New file @@ -0,0 +1,262 @@ # 7.9.0 技术设计方案:容器内 App 手动排序 ## 1. 目标 在现有 TagLauncher App Grid 中支持同一容器内 App 拖动排序,并将顺序作为“分类与布局方案”的一部分持久化。 ## 2. 当前架构现状 - `AppInfo` 不包含用户顺序字段,定义在 `src/Apptag/DataLayer.swift`。 - `TagDatabase.Store` 当前有 `appTags` 和 `tagOrder`,没有容器内 App 顺序字段。 - `AppIndexer.group()` 负责按 tag 分组,组内 App 顺序来自扫描结果 append 顺序。 - `ContentView.makeDisplayGroups()` 将分组名本地化后传给 App Grid。 - `AppGridCollectionView` 是 `NSViewRepresentable` + AppKit 自绘 grid。 - `AppGridGroupCardView` 按 `group.apps.indices` 布局 icon。 - `AppGridIconNSView` 当前长按拖动表示跨 tag 移动/复制,不包含目标插入 index。 ## 3. 设计原则 - 顺序是数据,不是临时 UI 状态。 - 容器 ID 必须稳定,不能依赖本地化显示名。 - 同容器排序不改变 `appTags`。 - 旧拖拽语义优先保护,不为排序重写跨容器拖动。 - 第一版以 path 作为 App 顺序身份,降低实现复杂度。 - 不存像素坐标、row/column、`IndexPath`,因为布局会随窗口尺寸、displayMode、iconSize 改变。 ## 4. 数据模型 ### 4.1 Store 新字段 建议在 `TagDatabase.Store` 增加: ```swift var containerAppOrder: [String: [String]] = [:] ``` 含义: - key:稳定容器 ID。 - value:该容器内 app path 的完整或部分顺序。 旧数据兼容: ```swift containerAppOrder = try container.decodeIfPresent( [String: [String]].self, forKey: .containerAppOrder ) ?? [:] ``` ### 4.2 容器 ID 建议新增 helper,统一生成容器 ID: ```swift enum AppContainerID { static let uncategorized = "__container.uncategorized" static let appleBuiltIn = "__container.appleBuiltIn" static func tag(_ name: String) -> String { "tag:\(name)" } static func system(_ id: SmartCategoryID) -> String { "system:\(id.rawValue)" } } ``` 第一版可以先支持普通 tag 和特殊容器,系统分类 ID 映射按现有 `TagDef.systemCategoryID` 补齐。 ### 4.3 TagGroup 扩展 当前 `TagGroup.id` 使用 `name`。建议增加: ```swift struct TagGroup: Identifiable { var id: String { containerID } let containerID: String let name: String let apps: [AppInfo] } ``` 这样 UI 显示名可以本地化,但排序和滚动定位使用稳定 ID。 兼容注意: - 现有按 `group.name` 判断特殊组的逻辑要逐步改为 container ID。 - 滚动目标如果仍传显示名,需增加兼容查找。 ## 5. 排序算法 输入: - 某个容器原始 apps。 - `containerAppOrder[containerID]`。 输出: - 已命中顺序的 App 先按记录顺序排列。 - 未命中记录的新 App 追加到后面,按现有默认名称排序。 - 顺序字段中不存在的 path 忽略,可在保存或 reconcile 时清理。 伪代码: ```swift func applyAppOrder(apps: [AppInfo], order: [String]) -> [AppInfo] { let byPath = Dictionary(uniqueKeysWithValues: apps.map { ($0.path.path, $0) }) let ordered = order.compactMap { byPath[$0] } let orderedPaths = Set(ordered.map { $0.path.path }) let remaining = apps .filter { !orderedPaths.contains($0.path.path) } .sorted { $0.name.localizedStandardCompare($1.name) == .orderedAscending } return ordered + remaining } ``` ## 6. 写入 API 建议新增: ```swift extension TagEditor { static func reorderApps(inContainer containerID: String, orderedPaths: [String]) } ``` 写入规则: - 只保存当前容器内可见 App 的完整 path 顺序。 - 去重。 - 过滤空 path。 - 如结果等于当前有效顺序则不写。 - 走 `TagDatabase.saveUserCategorySchemeMutation(reason: "reorder-apps")`。 ## 7. 分类方案与导入导出 必须更新: - `TagDatabase.Store.CodingKeys`:加入 `containerAppOrder`。 - `CategorySchemeFingerprint`:加入规范化后的 `containerAppOrder`。 - `exportTo` / `importFrom`:自动随 Store 编码,但要补 roundtrip QA。 - `restore(fromBackupAt:)`:恢复顺序字段。 - `resetAppTagAssignmentsToUncategorized()`:清理或重建顺序字段,建议清空旧普通容器顺序。 - `SmartStartService.applyDraft(replaceExistingScheme: true)`:清空旧顺序。 - tag rename/delete/relocalize:迁移或清理 `tag:<name>` 对应顺序。 - reconcile removed apps:清理顺序字段里的 orphan path。 ## 8. UI 集成 ### 8.1 回调 `AppGridCollectionView` 增加: ```swift let onReorderApps: (String, [String]) -> Void ``` 其中: - 第一个参数为 `containerID`。 - 第二个参数为排序后的 app path 列表。 `ContentView` 接收后调用 `TagEditor.reorderApps`,再 `refreshApps(forceLayoutRefresh: true)` 或局部刷新。 ### 8.2 Hit-test 在 `AppGridGroupCardView` 内根据鼠标位置计算 insertion index: - 使用当前 `iconViews` frame。 - 同容器内拖动时显示插入线/占位。 - 松手时生成新 path 顺序。 - 拖出当前 group 或命中其他 drop target 时走旧拖拽逻辑。 ### 8.3 拖拽 intent 现有 payload 是 `path + sourceTag` 字符串,不适合继续扩展复杂状态。建议内部引入结构化状态: ```swift struct AppDragPayload { let path: String let sourceContainerID: String let sourceDisplayName: String } ``` 第一版如果为了控制改动范围暂不完全替换 `AppDragCoordinator` payload,也必须在同容器排序分支中独立判断 source/target 和 insertion index,避免误触发旧 drop。 ### 8.4 状态清理 排序开始: - 抑制 hover bubble。 - 结束 tag nav reorder。 - 标记 app drag mode active。 排序结束或取消: - 清除插入提示。 - 恢复 bubble 状态。 - 清理 drag image / hover target。 ## 9. 不改范围 - 不改 Quick Search rank。 - 不改 App 扫描规则。 - 不改 SmartStart 分类匹配逻辑。 - 不改跨容器拖动的标签归属规则。 - 不改设置页现有数据面板结构。 ## 10. 迁移策略 - 不需要一次性迁移用户数据。 - 新字段缺省为空,表示使用旧默认顺序。 - 第一次用户在某容器排序后,只写该容器顺序。 - 版本升级到 `7.9.0` 时在 changelog 说明新增布局顺序字段。 ## 11. 开发顺序 1. 数据层: - Store 字段、容器 ID、排序 helper、写入 API、fingerprint、导入导出/恢复/重置/SmartStart 处理。 - 配套数据 QA。 2. UI 层: - 同容器 insertion index hit-test。 - 插入提示。 - 本地顺序预览。 3. 集成: - drop/end 写入。 - refresh 后顺序保持。 - 旧拖拽行为回归。 4. 发布: - 版本号 `7.9.0`。 - `CHANGELOG.md`。 - DMG / App Store 资料按实际发布目标准备。 ## 12. 风险与控制 | 风险 | 等级 | 控制 | | --- | --- | --- | | 顺序未持久化导致刷新丢失 | 高 | 数据层先行,QA roundtrip | | 本地化显示名作为 key 导致语言切换错位 | 高 | 统一 container ID | | 同容器排序误触发跨标签移动 | 高 | 独立 intent 和 hit-test | | 导入导出/恢复漏字段 | 高 | fingerprint + export/import QA | | 拖拽状态残留 | 中 | 统一 cancel/cleanup | | 新字段污染旧用户默认顺序 | 中 | 空字段即旧排序 | ## 13. 必跑验证 ```bash cd /Users/ar/Projects/Taglauncher/src bash Scripts/macos14_availability_typecheck_qa.sh bash Scripts/macos14_build_metadata_qa.sh bash Scripts/quick_search_app_name_qa.sh bash Scripts/quick_search_system_app_qa.sh bash Scripts/smartstart_catalog_resource_qa.sh bash Scripts/window_logic_qa.sh ``` 同时必须做真实鼠标手工验证,覆盖同容器排序、跨容器拖动、Option 复制、拖空白移除、macOS 14、5 种 displayMode 和 3 种标签位置。 docs/7.90/README.md
@@ -21,6 +21,9 @@ - [01-容器内App手动排序-TODO.md](./01-容器内App手动排序-TODO.md):可执行开发 TODO、切分和 QA 阻断项。 - [02-专题讨论纪要.md](./02-专题讨论纪要.md):2 个架构师、1 个代码审核员、1 个 QA 的专题讨论结论。 - [03-产品PRD.md](./03-产品PRD.md):产品目标、范围、交互、验收标准和发布要求。 - [04-QA测试用例集.md](./04-QA测试用例集.md):QA 测试矩阵、自动化用例、手工用例和发布阻断条件。 - [05-技术设计方案.md](./05-技术设计方案.md):数据模型、UI 集成、迁移策略和技术风险控制。 ## 后续记录规则