在现有 TagLauncher App Grid 中支持同一容器内 App 拖动排序,并将顺序作为“分类与布局方案”的一部分持久化。
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。appTags。IndexPath,因为布局会随窗口尺寸、displayMode、iconSize 改变。建议在 TagDatabase.Store 增加:
var containerAppOrder: [String: [String]] = [:]
含义:
旧数据兼容:
containerAppOrder = try container.decodeIfPresent(
[String: [String]].self,
forKey: .containerAppOrder
) ?? [:]
建议新增 helper,统一生成容器 ID:
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 补齐。
当前 TagGroup.id 使用 name。建议增加:
struct TagGroup: Identifiable {
var id: String { containerID }
let containerID: String
let name: String
let apps: [AppInfo]
}
这样 UI 显示名可以本地化,但排序和滚动定位使用稳定 ID。
兼容注意:
group.name 判断特殊组的逻辑要逐步改为 container ID。输入:
containerAppOrder[containerID]。输出:
伪代码:
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
}
建议新增:
extension TagEditor {
static func reorderApps(inContainer containerID: String, orderedPaths: [String])
}
写入规则:
TagDatabase.saveUserCategorySchemeMutation(reason: "reorder-apps")。必须更新:
TagDatabase.Store.CodingKeys:加入 containerAppOrder。CategorySchemeFingerprint:加入规范化后的 containerAppOrder。exportTo / importFrom:自动随 Store 编码,但要补 roundtrip QA。restore(fromBackupAt:):恢复顺序字段。resetAppTagAssignmentsToUncategorized():清理或重建顺序字段,建议清空旧普通容器顺序。SmartStartService.applyDraft(replaceExistingScheme: true):清空旧顺序。tag:<name> 对应顺序。AppGridCollectionView 增加:
let onReorderApps: (String, [String]) -> Void
其中:
containerID。ContentView 接收后调用 TagEditor.reorderApps,再 refreshApps(forceLayoutRefresh: true) 或局部刷新。
在 AppGridGroupCardView 内根据鼠标位置计算 insertion index:
iconViews frame。现有 payload 是 path + sourceTag 字符串,不适合继续扩展复杂状态。建议内部引入结构化状态:
struct AppDragPayload {
let path: String
let sourceContainerID: String
let sourceDisplayName: String
}
第一版如果为了控制改动范围暂不完全替换 AppDragCoordinator payload,也必须在同容器排序分支中独立判断 source/target 和 insertion index,避免误触发旧 drop。
排序开始:
排序结束或取消:
7.9.0 时在 changelog 说明新增布局顺序字段。7.9.0。CHANGELOG.md。| 风险 | 等级 | 控制 |
|---|---|---|
| 顺序未持久化导致刷新丢失 | 高 | 数据层先行,QA roundtrip |
| 本地化显示名作为 key 导致语言切换错位 | 高 | 统一 container ID |
| 同容器排序误触发跨标签移动 | 高 | 独立 intent 和 hit-test |
| 导入导出/恢复漏字段 | 高 | fingerprint + export/import QA |
| 拖拽状态残留 | 中 | 统一 cancel/cleanup |
| 新字段污染旧用户默认顺序 | 中 | 空字段即旧排序 |
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 种标签位置。