| CODEGRAPH.md | ●●●●● patch | view | raw | blame | history | |
| src/Apptag/ContentView.swift | ●●●●● patch | view | raw | blame | history | |
| src/Apptag/DataLayer.swift | ●●●●● patch | view | raw | blame | history | |
| src/Apptag/Info.plist | ●●●●● patch | view | raw | blame | history | |
| src/Apptag/PreferencesView.swift | ●●●●● patch | view | raw | blame | history | |
| src/CHANGELOG.md | ●●●●● patch | view | raw | blame | history | |
| src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md | ●●●●● patch | view | raw | blame | history | |
| src/Scripts/pro_custom_container_quota_qa.sh | ●●●●● patch | view | raw | blame | history |
CODEGRAPH.md
@@ -42,7 +42,7 @@ - Theme tab 只写视觉偏好 `appGridThemeID` 和炫彩主题调色值,炫彩滑轨只能影响 AppGrid 背景,不得改变标签、排序、备注或分类数据。 - 免费用户预览 Pro 主题时,Theme tab 状态行和当前预览主题卡片显示基于 `ProThemePreviewState.endsAt` 推导的 `MM:SS` 倒计时;倒计时到期只退出预览,不弹阻塞提示。 - Pro tab 展示免费版 / Pro 版对比表;对比表不显示独立标题,Pro 列标题前必须有线型皇冠标识。 - Pro tab 对比表包含自定义标签颜色、彩色容器 / 彩色网格、高级主题、自定义快捷键、自定义应用备注和数据导入导出 / 分类与布局备份恢复;免费列锁定功能显示“不可用”,彩色容器 / 彩色网格和高级主题显示“只可预览 5 分钟”,备注显示“免费 3 个”;Pro 列未购买时显示带勾的“支持”,备注行显示带勾的“无限备注”,已购买后功能行显示带勾的“已解锁”。表格内免费列和 Pro 列的表头及状态胶囊都在列内左对齐;所有状态胶囊使用与“免费 3 个”一致的中性样式,不显示皇冠图标或黄色 Pro 背景。 - Pro tab 对比表包含可维护标签数量、容器内 App 图标位置排序、自定义标签颜色、彩色容器 / 彩色网格、高级主题、自定义快捷键、自定义应用备注和数据导入导出 / 分类与布局备份恢复;标签列表自身顺序不列入 Pro 对比,因为免费用户也可持久保存。免费列锁定功能显示“不可用”,彩色容器 / 彩色网格和高级主题显示“只可预览 5 分钟”,备注显示“免费 3 个”;Pro 列未购买时显示带勾的“支持”,备注行显示带勾的“无限备注”,已购买后功能行显示带勾的“已解锁”。表格内免费列和 Pro 列的表头及状态胶囊都在列内左对齐;所有状态胶囊使用与“免费 3 个”一致的中性样式,不显示皇冠图标或黄色 Pro 背景。 - About tab 保留顶部固定 Pro 状态行;主内容只保留应用信息、帮助文档和联系信息,不放 Pro 汇总卡或免费 / Pro 差异表。 ## AppGrid 显示与交互 @@ -202,7 +202,7 @@ - `src/Scripts/pro_localization_qa.sh` - Pro 文案 29 语种静态门禁;检查 `pro.*`、设置页 Pro 状态/对比表、自定义标签颜色、Quick Search 快捷键页和气泡提示设置键完整、占位符完整、无生成污染和非英文语言无英文兜底复制。 - `src/Scripts/pro_feature_gate_qa.sh` - Pro 功能源码门禁;检查 StoreKit 权益、老用户自动 Pro、主题门禁、导入导出硬拦截、3 条备注额度、自定义标签颜色、自定义快捷键、排序预览、Pro UI 提示、设置页线型图标、轻量 Pro 权益行、Pro 页签、免费列“不支持”状态和 About 页不回归 Pro 汇总卡。 - Pro 功能源码门禁;检查 StoreKit 权益、老用户自动 Pro、主题门禁、导入导出硬拦截、3 条备注额度、自定义标签颜色、自定义快捷键、容器内 App 排序预览、Pro UI 提示、设置页线型图标、轻量 Pro 权益行、Pro 页签、免费列“不支持”状态和 About 页不回归 Pro 汇总卡。 - `src/Scripts/pro_theme_preview_countdown_qa.sh` - Pro 主题 / 彩色显示模式预览倒计时门禁;检查状态行倒计时、当前预览卡片 `MM:SS`、到期清理、29 语种 `%time%` 占位符和禁止写死体验时长。 - `src/Scripts/pro_display_mode_preview_qa.sh` @@ -227,7 +227,7 @@ - 不得擅自改变源码根目录边界:App 源码只在 `src/` 下维护。 - 不得把主题、视觉偏好、窗口状态混入 `TagDatabase` 的分类/标签业务数据。 - 主题变化只允许影响 AppGrid 视觉,不得改变标签数据、排序、备注、SmartStart/category scheme、Quick Search、拖拽语义或导入导出。 - Pro 门禁不得写脏免费用户真实数据:主题预览不写 `appGridThemeID`,排序预览不写 `containerAppOrder`,第 6 个新备注不写 `appNotes` / `appNoteMetadata`,导入 / 导出拦截必须早于文件面板和文件读写。 - Pro 门禁不得写脏免费用户真实数据:主题预览不写 `appGridThemeID`,容器内 App 排序预览不写 `containerAppOrder`,第 6 个新备注不写 `appNotes` / `appNoteMetadata`,导入 / 导出拦截必须早于文件面板和文件读写。 - 自定义标签颜色是 Pro 功能:免费用户可继续使用基础色,已有自定义色继续显示,不能新增或修改为新自定义色;改回基础色必须允许且会清除 `customColor`。 - Pro 状态 UI 必须优先使用设置页签下方固定位置的统一 header;不得在各 tab 内重复塞第二条 Pro 引导。About 不承载 Pro 汇总卡或免费 / Pro 差异说明,Data 不承载营销式大卡,避免挤压数据管理布局。 - 免费主题固定为默认、深蓝、黑色;其它主题只能预览或在 Pro 解锁后持久化。 src/Apptag/ContentView.swift
@@ -1630,7 +1630,7 @@ .onEnded { _ in guard canReorderTag(tagName) else { return } dragItem = nil persistTagOrderIfAllowed() persistTagOrder() } } @@ -2669,22 +2669,15 @@ let hadDragState = tagNavDragModeActive || tagNavDragItem != nil guard hadDragState else { return } if tagNavDragModeActive && tagNavReorderDidMove { persistTagOrderIfAllowed() persistTagOrder() } tagNavDragModeActive = false tagNavDragItem = nil tagNavReorderDidMove = false } private func persistTagOrderIfAllowed() { guard proEntitlement.isUnlocked else { showDropWarning(message: tr("pro.tagSorting.previewToast")) return } guard TagEditor.reorderTags(draggedTagNames) else { showDropWarning(message: tr("pro.tagSorting.previewToast")) return } private func persistTagOrder() { _ = TagEditor.reorderTags(draggedTagNames) } private func cancelTagNavReorderVisualState() { src/Apptag/DataLayer.swift
@@ -1943,7 +1943,6 @@ /// Persist a new tag display order. @discardableResult static func reorderTags(_ names: [String]) -> Bool { guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false } var store = TagDatabase.load() let previousStore = store store.tagOrder = names src/Apptag/Info.plist
@@ -19,7 +19,7 @@ <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>8.3.2</string> <string>8.3.5</string> <key>CFBundleURLTypes</key> <array> <dict> @@ -32,7 +32,7 @@ </dict> </array> <key>CFBundleVersion</key> <string>20260702.0325</string> <string>20260702.1046</string> <key>LSApplicationCategoryType</key> <string>public.app-category.utilities</string> <key>LSMinimumSystemVersion</key> src/Apptag/PreferencesView.swift
@@ -1049,15 +1049,6 @@ proUnlockedStyle: .neutral ), ProTabComparisonFeature( title: tr("pro.feature.tagOrderPersistence"), freeStatusKey: "settings.proStatus.previewOnly", proLockedStatusKey: "settings.proStatus.supported", proUnlockedStatusKey: "settings.proStatus.supported", freeStyle: .neutral, proLockedStyle: .neutral, proUnlockedStyle: .neutral ), ProTabComparisonFeature( title: tr("pro.feature.containerAppOrderPersistence"), freeStatusKey: "settings.proStatus.previewOnly", proLockedStatusKey: "settings.proStatus.supported", src/CHANGELOG.md
@@ -2,6 +2,12 @@ ## [Unreleased] ## [8.3.5] — 2026-07-02 - 标签列表自身顺序不再作为 Pro 权益:免费用户拖动标签后也会持久保存容器显示顺序 - Pro 页免费 / Pro 对比表移除“自定义排序:标签在列表中的位置”这一行,保留容器内 App 图标位置排序、自定义快捷键等其它 Pro 权益 - 版本号更新为 `8.3.5`,Build 更新为 `20260702.1046` ## [8.3.2] — 2026-07-02 - 修复 App Store 从付费下载切换为免费下载 + Pro 内购时的旧付费用户迁移边界:`8.3.2` 之前通过 App Store 付费下载过 TagLauncher 的用户会自动保留 Pro 权益 src/Docs/Requirements/2026-07-01-pro-custom-container-quota-prd.md
@@ -1,18 +1,18 @@ # PRD:自定义标签额度、排序保存与 Pro 转化 日期:2026-07-01 状态:产品方案讨论稿,待研发评审与实现 状态:已实现,2026-07-02 按 8.3.5 版本决策修订 适用产品:TagLauncher macOS App ## 1. 功能名 免费版自定义标签额度、标签排序预览与 Pro 无限容器管理 免费版自定义标签额度与 Pro 无限容器管理 ## 2. 需求描述 TagLauncher 的核心价值是用标签 / 容器管理 Mac 应用。为了提高 Pro 付费转化,需要把 Pro 权益更直接地绑定到核心功能上:免费用户可以体验默认智能分类和基础整理能力,但当用户开始搭建自己的长期容器体系时,应自然触发 Pro 升级理由。 本需求不采用“维护次数”限制。免费版限制的是可个性化维护的自定义标签 / 容器数量,以及标签拖动排序的保存能力。 本需求不采用“维护次数”限制。免费版限制的是可个性化维护的自定义标签 / 容器数量;标签列表自身拖动排序不再作为 Pro 权益,免费用户也可以保存容器显示顺序。 ## 3. 背景与目标 @@ -26,12 +26,12 @@ | 目标 | 说明 | |:---|:---| | 提升 Pro 转化 | 把升级触点放在用户开始扩展自定义容器体系、希望沉淀长期排序习惯时。 | | 提升 Pro 转化 | 把升级触点放在用户开始扩展自定义容器体系、需要容器内 App 图标排序等高级维护能力时。 | | 保留新用户激活价值 | 默认智能分类仍可免费使用,避免下载后立刻被付费墙挡住。 | | 规则可解释 | 用户应能理解“免费可维护 6 个自定义容器,Pro 不限”。 | | 避免数据被扣押 | 老用户和导入数据不能被删除或隐藏;超额时应保留查看和启动能力。 | | 避免惩罚试错 | 不限制改名、改色等操作次数,避免用户不敢整理。 | | 保持体验可感知 | 免费用户可以预览标签拖动排序效果,但保存长期排序结果需要 Pro。 | | 保持体验可感知 | 免费用户可以保存标签列表自身顺序;容器内 App 图标排序仍沿用免费预览、Pro 保存的规则。 | ## 4. 术语定义 @@ -43,8 +43,8 @@ | 自定义维护容器 | 用户对默认智能分类进行个性化维护后形成的容器,或用户自建容器。 | 计入 | | 个性化维护 | 会让默认分类变成用户长期自定义容器的操作,例如改名、改基础颜色、手动调整应用归属。 | 会触发计入 | | 减法操作 | 删除、隐藏、清理、合并,目的是减少或恢复到免费额度内。 | 不新增额度 | | 标签拖动排序预览 | 用户拖动标签并看到临时顺序变化,但该顺序不作为长期结果保存。 | 不计入 | | 标签排序保存 | 用户拖动标签后的顺序被保留,并在后续打开、刷新或重启后继续生效。 | Pro 权益 | | 标签列表排序保存 | 用户拖动标签后的容器显示顺序被保留,并在后续打开、刷新或重启后继续生效。 | 免费可用 | | 容器内 App 图标排序保存 | 用户拖动容器内 App 图标后的顺序被长期保留。 | Pro 权益 | ## 5. 产品结论 @@ -60,7 +60,21 @@ Pro 用户可维护不限数量的自定义标签 / 容器。 标签拖动排序的保存能力属于 Pro 权益。免费用户可以拖动标签预览排序效果,但不能保存新的标签顺序。 标签列表自身排序保存能力对免费用户开放。免费用户拖动标签后也会保存新的容器显示顺序。 容器内 App 图标排序保存仍属于 Pro 权益。免费用户可以预览容器内 App 图标排序效果,但不能保存新的容器内图标顺序。 ### 5.1.1 2026-07-02 / 8.3.5 决策更新 用户反馈后确认:`自定义排序:标签在列表中的位置(=容器显示位置)` 不再作为 Pro 锁定项。 研发与 QA 口径: - Pro 对比页删除这一行。 - `TagEditor.reorderTags` 不再检查 Pro 权益,免费用户也可保存标签列表顺序。 - `ContentView` 不再在标签拖动结束时展示“仅预览 / 升级 Pro”提示。 - `自定义排序:容器内 App 图标位置` 仍保留 Pro 权益,不在本次调整中放开。 - `自定义快捷键` 仍保留 Pro 权益,不在本次调整中放开。 ### 5.2 不采用方案 @@ -251,33 +265,27 @@ | 功能 | 免费版 | Pro 版 | |:---|:---|:---| | 自定义标签 / 容器维护 | 6 个 | 不限数量 | | 标签排序保存 | 仅预览 | 可保存 | | 标签列表排序保存 | 可保存 | 可保存 | | 容器内 App 图标排序保存 | 仅预览 | 可保存 | ### 10.7 标签拖动排序保存 ### 10.7 标签列表拖动排序保存 标签拖动排序应作为独立的 Pro 权益处理,交互参考已实现的“在容器内部拖动改变 App 图标顺序,免费版用户只能预览”。 标签列表自身拖动排序免费可用。用户拖动标签后,新的容器显示顺序应持久保存。 | 用户状态 | 行为 | |:---|:---| | 免费用户 | 可以拖动标签,并看到临时排序效果。 | | 免费用户拖动结束 | 不保存新的标签顺序,展示 Pro 提示。 | | 免费用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序恢复为当前已保存顺序。 | | 免费用户 | 可以拖动标签,并保存新的标签顺序。 | | 免费用户拖动结束 | 保存新的标签顺序,不展示 Pro 提示。 | | 免费用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序保持为上次保存结果。 | | Pro 用户 | 可以拖动标签,拖动结束后保存新的标签顺序。 | | Pro 用户重新打开 / 刷新 / 触发标签列表重建 | 标签顺序保持为上次保存结果。 | 免费用户的提示方式应与现有 App 图标排序预览提示保持一致,不新增独立视觉样式。 建议文案: > 当前仅为预览。升级 Pro 后可保存标签排序。 交互边界: - 不在拖动开始前阻断用户。 - 免费用户拖动过程应保持顺畅,可以看到排序预览。 - 拖动结束后不保存新的顺序。 - 免费用户拖动过程应保持顺畅,拖动结束后保存新的顺序。 - 历史已保存的标签顺序不被清空。 - 本需求不改变容器内部 App 图标排序的现有规则。 - 本需求不改变容器内部 App 图标排序的现有 Pro 规则。 ## 11. 用户旅程 @@ -312,13 +320,13 @@ 4. 用户尝试新增第 11 个标签时,提示升级 Pro。 5. 用户可删除或合并标签,回到免费额度内。 ### 11.5 免费用户预览标签排序 ### 11.5 免费用户保存标签列表排序 1. 免费用户希望调整标签展示顺序。 2. 用户拖动标签,界面展示拖动后的临时排序效果。 2. 用户拖动标签,界面展示拖动后的排序效果。 3. 用户松手完成拖动。 4. 系统提示当前仅为预览,保存标签排序需要 Pro。 5. 用户重新打开、刷新或触发标签列表重建后,标签顺序恢复为当前已保存顺序。 4. 系统保存新的标签顺序。 5. 用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。 ### 11.6 Pro 用户保存标签排序 @@ -337,8 +345,8 @@ | TAG-QUOTA-04 | 作为免费用户,我希望删除或合并标签后恢复额度,以便继续整理。 | 删除自定义维护容器后释放额度。 | | TAG-QUOTA-05 | 作为 Pro 用户,我希望不受标签数量限制,以便建立完整应用容器体系。 | Pro 用户可创建和维护不限数量的自定义标签。 | | TAG-QUOTA-06 | 作为老用户,我希望已有标签不被删除,以便继续访问我的整理结果。 | 超额历史标签仍可查看和启动。 | | TAG-SORT-01 | 作为免费用户,我希望能拖动标签看看排序效果,以便判断升级 Pro 后是否有价值。 | 免费用户可拖动标签并看到临时排序效果。 | | TAG-SORT-02 | 作为免费用户,我希望知道为什么排序没有保存,以便理解 Pro 权益边界。 | 免费用户拖动结束后看到“仅预览,保存需 Pro”的提示。 | | TAG-SORT-01 | 作为免费用户,我希望能保存标签列表顺序,以便按自己的浏览习惯组织容器。 | 免费用户拖动标签后,新的标签顺序被保存并持续生效。 | | TAG-SORT-02 | 作为免费用户,我希望标签列表排序不会突然弹出 Pro 提示,以便保持整理流程顺畅。 | 免费用户拖动标签结束后不出现标签排序 Pro 提示。 | | TAG-SORT-03 | 作为 Pro 用户,我希望保存自定义标签顺序,以便长期保持自己的容器浏览习惯。 | Pro 用户拖动标签后,新的标签顺序被保存并持续生效。 | ## 13. 验收标准 @@ -357,10 +365,10 @@ | QA-TQ-10 | Pro 页功能对比展示“自定义标签 / 容器维护:免费 6 个,Pro 不限”。 | | QA-TQ-11 | 免费用户可看到自定义容器额度计数,例如“已使用 X / 6”或“当前还可维护 X 个”。 | | QA-TQ-12 | 触发第 7 个自定义维护动作时不创建临时标签、不保存临时改动、不进入预览,只展示“升级 Pro”和“管理已有标签”。 | | QA-TS-01 | 免费用户可以拖动标签,并看到排序预览效果。 | | QA-TS-02 | 免费用户拖动标签结束后,不保存新的标签顺序。 | | QA-TS-03 | 免费用户重新打开、刷新或触发标签列表重建后,标签顺序恢复为当前已保存顺序。 | | QA-TS-04 | 免费用户拖动标签结束后,看到与 App 图标排序预览一致风格的 Pro 提示。 | | QA-TS-01 | 免费用户可以拖动标签,并保存新的标签顺序。 | | QA-TS-02 | 免费用户拖动标签结束后,不出现标签排序 Pro 提示。 | | QA-TS-03 | 免费用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。 | | QA-TS-04 | Pro 页免费 / Pro 对比表不再展示“自定义排序:标签在列表中的位置”。 | | QA-TS-05 | Pro 用户拖动标签结束后,新的标签顺序被保存。 | | QA-TS-06 | Pro 用户重新打开、刷新或触发标签列表重建后,标签顺序保持为上次保存结果。 | | QA-TS-07 | 历史已保存的标签顺序不因本需求上线被清空。 | @@ -375,7 +383,7 @@ | 自动删除超额标签 | 不删除、不隐藏老用户或导入数据中的超额标签。 | | 标签图标能力 | 本需求不新增标签图标或上传图标。 | | App 图标排序规则 | 不调整容器内部 App 图标排序的现有 Pro 预览 / 保存规则。 | | 新增排序视觉样式 | 不为标签排序新增一套独立提示视觉,沿用现有 Pro 提示体系。 | | 新增排序视觉样式 | 不为容器内 App 图标排序新增一套独立提示视觉,沿用现有 Pro 提示体系。 | | 订阅模式 | 本需求不改变 Pro 一次性买断策略。 | ## 15. 指标建议 @@ -388,8 +396,8 @@ | 触达后 7 天购买率 | 衡量该权益对转化的真实贡献。 | | 触达后流失率 / 卸载率 | 监控限制是否过重。 | | 删除 / 合并标签行为 | 判断用户是否能理解并管理额度。 | | 标签排序预览触达率 | 衡量免费用户是否会尝试调整标签顺序。 | | 标签排序预览后的 Pro 点击率 | 衡量“保存标签排序”权益是否具备转化价值。 | | 容器内 App 图标排序预览触达率 | 衡量免费用户是否会尝试调整容器内图标顺序。 | | 容器内 App 图标排序预览后的 Pro 点击率 | 衡量“保存容器内 App 图标排序”权益是否具备转化价值。 | ## 16. 决策记录 src/Scripts/pro_custom_container_quota_qa.sh
@@ -26,6 +26,25 @@ fail(message) def function_body(source: str, signature: str) -> str: start = source.find(signature) if start == -1: fail(f"missing function signature: {signature}") brace = source.find("{", start) if brace == -1: fail(f"missing function body for: {signature}") depth = 0 for index in range(brace, len(source)): char = source[index] if char == "{": depth += 1 elif char == "}": depth -= 1 if depth == 0: return source[brace + 1:index] fail(f"unterminated function body for: {signature}") pro = read("ProEntitlement.swift") data_layer = read("DataLayer.swift") tag_editor = read("TagEditorView.swift") @@ -57,7 +76,9 @@ require(data_layer, "static func moveApp(path: String, from sourceTag: String, to targetTag: String, color: Int, copy: Bool) -> TagEditorMutationResult", "moveApp must return a mutation result") require(data_layer, "TagDatabase.markCustomMaintainedIfNeeded", "membership changes must mark maintained system tags") require(data_layer, "static func reorderTags(_ names: [String]) -> Bool", "tag order persistence must return whether it saved") require(data_layer, "guard ProEntitlementPolicy.isUnlocked(.persistentAppSorting) else { return false }", "tag order persistence must be Pro-gated") reorder_tags_body = function_body(data_layer, "static func reorderTags(_ names: [String]) -> Bool") if "ProEntitlementPolicy.isUnlocked(.persistentAppSorting)" in reorder_tags_body: fail("tag list order persistence must be available to free users") require(tag_editor, "onCustomContainerQuotaExceeded", "TagEditorView must expose quota-blocked callback") require(tag_editor, "onCustomContainerQuotaStatusChanged", "TagEditorView must expose quota status refresh callback") @@ -66,8 +87,10 @@ require(tag_editor, "guard handleMutationResult(result) else { return }", "TagEditorView must not update local state before a save succeeds") require(tag_editor, "blockedCustomContainerQuota", "TagEditorView must surface quota blocks") require(content, "persistTagOrderIfAllowed()", "ContentView must route tag order save through a Pro gate") require(content, "pro.tagSorting.previewToast", "free tag sorting preview toast is missing") require(content, "persistTagOrder()", "ContentView must persist tag list order") persist_tag_order_body = function_body(content, "private func persistTagOrder()") if "proEntitlement.isUnlocked" in persist_tag_order_body or "pro.tagSorting.previewToast" in persist_tag_order_body: fail("free users must be able to persist tag list order without a Pro preview toast") require(content, "presentCustomContainerQuotaPrompt", "ContentView must show custom container quota prompt") require(content, "customContainerQuotaAccessory", "ContentView tag editor must show quota count") require(content, "@State private var customContainerQuotaStatus = ProEntitlementPolicy.customContainerQuotaStatus()", "ContentView quota count must be backed by SwiftUI state") @@ -84,19 +107,19 @@ require(preferences, "onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus", "Settings must refresh quota count after TagEditorView saves") require(preferences, "settingsCustomContainerQuotaOverviewStatus = quotaStatus", "Settings scanApps must initialize quota count from the same store") require(preferences, 'title: tr("pro.feature.maintainableTagCount")', "Pro comparison must include maintainable tag count row") require(preferences, 'title: tr("pro.feature.tagOrderPersistence")', "Pro comparison must include tag order row") if 'title: tr("pro.feature.tagOrderPersistence")' in preferences: fail("Pro comparison must not list tag list order; it is available to free users") require(preferences, 'title: tr("pro.feature.containerAppOrderPersistence")', "Pro comparison must include container app order row") first_three = [ first_two = [ preferences.find('title: tr("pro.feature.maintainableTagCount")'), preferences.find('title: tr("pro.feature.tagOrderPersistence")'), preferences.find('title: tr("pro.feature.containerAppOrderPersistence")'), ] custom_color_index = preferences.find('title: tr("pro.feature.customTagColors")') if any(index == -1 for index in first_three) or custom_color_index == -1: if any(index == -1 for index in first_two) or custom_color_index == -1: fail("could not locate Pro comparison feature rows") if not (first_three[0] < first_three[1] < first_three[2] < custom_color_index): fail("new Pro comparison rows must be the first three rows") if not (first_two[0] < first_two[1] < custom_color_index): fail("Pro comparison rows must begin with tag quota and container app order") required_l10n = [ "pro.feature.customContainerQuota", @@ -106,9 +129,7 @@ "pro.customContainers.quotaUnlimitedShort", "pro.customContainers.quotaPrompt", "pro.customContainers.manageExisting", "pro.tagSorting.previewToast", "pro.feature.maintainableTagCount", "pro.feature.tagOrderPersistence", "pro.feature.containerAppOrderPersistence", "settings.proStatus.maxSixTags", "settings.proStatus.unlimitedTags", @@ -139,5 +160,5 @@ if zh_hans.get("settings.proStatus.unlimitedTags") != "✅没有限制": fail("zh-Hans unlimited tag status must include the approved leading checkmark") print("PASS Pro custom container quota QA: quota gate, tag-order preview, Pro table rows, and 29-language keys are wired") print("PASS Pro custom container quota QA: quota gate, free tag-order persistence, Pro table rows, and 29-language keys are wired") PY