Ariver
2026-06-29 18c90acda54cb46815a081ecfc36ea8f2b9e93fd
Fix custom hotkey settings UI for 8.2.1
4 files modified
4 files added
232 ■■■■ changed files
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/PreferencesView.swift 120 ●●●● patch | view | raw | blame | history
src/CHANGELOG.md 8 ●●●●● patch | view | raw | blame | history
src/Release/AppStore-8.2.1-20260628.2354/Archive/SHA256SUMS.txt 1 ●●●● patch | view | raw | blame | history
src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg patch | view | raw | blame | history
src/Release/AppStore-8.2.1-20260628.2354/QA/QA-Report.md 61 ●●●●● patch | view | raw | blame | history
src/Release/AppStore-8.2.1-20260628.2354/RELEASE_MANIFEST.md 25 ●●●●● patch | view | raw | blame | history
src/Scripts/pro_custom_hotkeys_qa.sh 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Info.plist
@@ -19,7 +19,7 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>8.2.0</string>
    <string>8.2.1</string>
    <key>CFBundleURLTypes</key>
    <array>
        <dict>
@@ -32,7 +32,7 @@
        </dict>
    </array>
    <key>CFBundleVersion</key>
    <string>20260628.2308</string>
    <string>20260628.2354</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/Apptag/PreferencesView.swift
@@ -944,6 +944,7 @@
                customizeTitle: proEntitlement.isUnlocked ? tr("hotkeys.customize") : tr("pro.card.badge"),
                restoreTitle: tr("hotkeys.restoreDefault"),
                isRecording: recordingHotkeyKind == .main,
                isProLocked: !proEntitlement.isUnlocked,
                canRestore: proEntitlement.isUnlocked && LauncherHotkeySettings.hasCustomHotkey(for: .main),
                onCustomize: { startHotkeyRecording(for: .main) },
                onRestore: { restoreDefaultHotkey(for: .main) }
@@ -966,6 +967,7 @@
                customizeTitle: proEntitlement.isUnlocked ? tr("hotkeys.customize") : tr("pro.card.badge"),
                restoreTitle: tr("hotkeys.restoreDefault"),
                isRecording: recordingHotkeyKind == .quickSearch,
                isProLocked: !proEntitlement.isUnlocked,
                canRestore: proEntitlement.isUnlocked && LauncherHotkeySettings.hasCustomHotkey(for: .quickSearch),
                onCustomize: { startHotkeyRecording(for: .quickSearch) },
                onRestore: { restoreDefaultHotkey(for: .quickSearch) }
@@ -1728,8 +1730,7 @@
    let statusTone: HotkeyStatusTone
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(alignment: .top, spacing: 12) {
        HStack(alignment: .center, spacing: 18) {
                VStack(alignment: .leading, spacing: 3) {
                    Text(title)
                        .font(.system(size: 13, weight: .semibold))
@@ -1740,20 +1741,16 @@
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                VStack(alignment: .leading, spacing: 4) {
            HStack(alignment: .center, spacing: 12) {
                    Text(displayText)
                        .font(.system(size: 15, weight: .semibold))
                        .lineLimit(1)
                        .minimumScaleFactor(0.85)
                    Text(statusText)
                        .font(.system(size: 11, weight: .semibold))
                        .foregroundStyle(statusTone.foreground)
                        .padding(.horizontal, 8)
                        .padding(.vertical, 3)
                        .background(Capsule().fill(statusTone.background))
                    .frame(width: 156, alignment: .leading)
                HotkeyStatusPill(text: statusText, tone: statusTone)
                }
                .frame(width: 180, alignment: .leading)
            }
            .frame(width: 310, alignment: .leading)
        }
        .padding(12)
        .background(
@@ -1776,13 +1773,13 @@
    let customizeTitle: String
    let restoreTitle: String
    let isRecording: Bool
    let isProLocked: Bool
    let canRestore: Bool
    let onCustomize: () -> Void
    let onRestore: () -> Void
    var body: some View {
        VStack(alignment: .leading, spacing: 12) {
            HStack(alignment: .top, spacing: 12) {
        HStack(alignment: .center, spacing: 18) {
                VStack(alignment: .leading, spacing: 3) {
                    Text(title)
                        .font(.system(size: 13, weight: .semibold))
@@ -1793,19 +1790,15 @@
                }
                .frame(maxWidth: .infinity, alignment: .leading)
                VStack(alignment: .leading, spacing: 7) {
            HStack(alignment: .center, spacing: 12) {
                VStack(alignment: .leading, spacing: 6) {
                    Text(isRecording ? "…" : displayText)
                        .font(.system(size: 15, weight: .semibold))
                        .lineLimit(1)
                        .minimumScaleFactor(0.85)
                    HStack(spacing: 6) {
                        Text(statusText)
                            .font(.system(size: 11, weight: .semibold))
                            .foregroundStyle(statusTone.foreground)
                            .padding(.horizontal, 8)
                            .padding(.vertical, 3)
                            .background(Capsule().fill(statusTone.background))
                        HotkeyStatusPill(text: statusText, tone: statusTone)
                        if isRecording {
                            Text(tr("hotkeys.recordingInline"))
@@ -1813,20 +1806,23 @@
                                .foregroundStyle(.blue)
                        }
                    }
                }
                .frame(width: 156, alignment: .leading)
                    HStack(spacing: 8) {
                        Button(customizeTitle, action: onCustomize)
                            .buttonStyle(.borderedProminent)
                            .controlSize(.small)
                HotkeyPrimaryActionButton(
                    title: customizeTitle,
                    isProLocked: isProLocked,
                    action: onCustomize
                )
                        Button(restoreTitle, action: onRestore)
                            .buttonStyle(.bordered)
                            .controlSize(.small)
                            .disabled(!canRestore)
                if canRestore {
                    HotkeySecondaryActionButton(
                        title: restoreTitle,
                        action: onRestore
                    )
                    }
                }
                .frame(width: 230, alignment: .leading)
            }
            .frame(width: 310, alignment: .leading)
        }
        .padding(12)
        .background(
@@ -1840,6 +1836,70 @@
    }
}
private struct HotkeyStatusPill: View {
    let text: String
    let tone: HotkeyStatusTone
    var body: some View {
        Text(text)
            .font(.system(size: 11, weight: .semibold))
            .foregroundStyle(tone.foreground)
            .lineLimit(1)
            .padding(.horizontal, 8)
            .padding(.vertical, 3)
            .background(Capsule().fill(tone.background))
    }
}
private struct HotkeyPrimaryActionButton: View {
    let title: String
    let isProLocked: Bool
    let action: () -> Void
    var body: some View {
        Button(action: action) {
            if isProLocked {
                ProStatusPill(text: title, style: .locked, compact: true)
            } else {
                Text(title)
                    .font(.system(size: 11, weight: .semibold))
                    .foregroundStyle(Color.white)
                    .lineLimit(1)
                    .padding(.horizontal, 12)
                    .padding(.vertical, 6)
                    .background(
                        Capsule(style: .continuous)
                            .fill(Color.accentColor)
                    )
            }
        }
        .buttonStyle(.plain)
        .contentShape(Capsule(style: .continuous))
    }
}
private struct HotkeySecondaryActionButton: View {
    let title: String
    let action: () -> Void
    var body: some View {
        Button(action: action) {
            Text(title)
                .font(.system(size: 11, weight: .semibold))
                .foregroundStyle(.secondary)
                .lineLimit(1)
                .padding(.horizontal, 12)
                .padding(.vertical, 6)
                .background(
                    Capsule(style: .continuous)
                        .fill(Color.secondary.opacity(0.12))
                )
        }
        .buttonStyle(.plain)
        .contentShape(Capsule(style: .continuous))
    }
}
private struct SettingsConfirmationView: View {
    let title: String
    let message: String
src/CHANGELOG.md
@@ -2,6 +2,14 @@
## [Unreleased]
## [8.2.1] — 2026-06-28
- 修复“设置 > 快捷键”页的自定义快捷键行排版:快捷键值、状态和操作入口改为同一行稳定布局,避免右侧控件纵向堆叠导致页面失衡
- 自定义快捷键未解锁入口统一改为全局皇冠 Pro pill,不再显示与其他 Pro 功能不一致的蓝色 `Pro` 按钮
- 未保存自定义快捷键时不再显示灰掉的“恢复默认”按钮,减少无效控件干扰;保存自定义值后仍可恢复默认
- 明确本机验收方式:使用 QA 环境变量 `TAGLAUNCHER_QA_PRO_STATE=pro` 可在不走真实 App Store 购买的情况下验证 Pro 自定义快捷键,关闭该变量后恢复真实权益状态
- 版本号更新为 `8.2.1`,Build 更新为 `20260628.2354`
## [8.2.0] — 2026-06-28
- 新增 Pro 自定义快捷键:Pro 用户可在“设置 > 快捷键”分别修改 App Grid 全局唤起和 Quick Search 全局唤起快捷键
src/Release/AppStore-8.2.1-20260628.2354/Archive/SHA256SUMS.txt
New file
@@ -0,0 +1 @@
f166743d18af08225056a77bbc61dd39c5ee4bd264273786d094bf941b18c271  src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg
src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg
Binary files differ
src/Release/AppStore-8.2.1-20260628.2354/QA/QA-Report.md
New file
@@ -0,0 +1,61 @@
# QA Report - TagLauncher 8.2.1 (20260628.2354)
日期:2026-06-28
## 范围
- 修复“设置 > 快捷键”页自定义快捷键行 UI 排版。
- 将未解锁自定义快捷键入口统一为全局皇冠 Pro pill。
- 隐藏无效的“恢复默认”按钮,仅在存在自定义快捷键时显示。
- 明确本机 QA 解锁 Pro 的验证方式,不改真实购买流程。
## 验证命令
```bash
bash src/Scripts/pro_custom_hotkeys_qa.sh
bash src/Scripts/pro_feature_gate_qa.sh
bash src/Scripts/pro_localization_qa.sh
bash src/Scripts/macos14_availability_typecheck_qa.sh
bash src/Scripts/macos14_build_metadata_qa.sh
APP_BUILD=20260628.2354 bash build.sh
APP_BUILD=20260628.2354 bash make_dmg.sh
codesign --verify --deep --strict --verbose=2 build/TagLauncher.app
hdiutil verify build/TagLauncher.dmg
```
## 结果
- Pro custom hotkeys QA:通过。
- Pro feature gate QA:通过。
- Pro localization QA:通过,29 个语种、49 个 Pro keys。
- macOS 14 availability typecheck:通过。
- macOS 14 build metadata:通过,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arm64`。
- App build:通过。
- DMG build:通过。
- codesign verify:通过。
- hdiutil verify:通过。
## 本机 Pro 验收方式
真实 App Store 购买不可用时,可以用 QA 环境变量验证 Pro 自定义快捷键,不写入真实购买记录。
启用 QA Pro:
```bash
launchctl setenv TAGLAUNCHER_QA_PRO_STATE pro
osascript -e 'quit app "TagLauncher"' >/dev/null 2>&1 || true
open -a /Applications/TagLauncher.app
```
恢复真实权益状态:
```bash
launchctl unsetenv TAGLAUNCHER_QA_PRO_STATE
osascript -e 'quit app "TagLauncher"' >/dev/null 2>&1 || true
open -a /Applications/TagLauncher.app
```
## 产物
- DMG:`src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg`
- SHA256:`f166743d18af08225056a77bbc61dd39c5ee4bd264273786d094bf941b18c271`
src/Release/AppStore-8.2.1-20260628.2354/RELEASE_MANIFEST.md
New file
@@ -0,0 +1,25 @@
# Release Manifest - TagLauncher 8.2.1 (20260628.2354)
日期:2026-06-28
## 版本
- App Version:8.2.1
- Build:20260628.2354
- 基线:8.2.0 / 20260628.2308
## 变更
- 修复快捷键设置页右侧操作区排版混乱。
- 自定义快捷键锁定入口统一使用皇冠 Pro pill。
- 未存在自定义快捷键时隐藏“恢复默认”按钮。
- 记录本机 QA Pro 解锁验收流程,避免依赖真实购买。
## 安装包
- `Archive/TagLauncher-8.2.1-build20260628.2354.dmg`
- SHA256:`f166743d18af08225056a77bbc61dd39c5ee4bd264273786d094bf941b18c271`
## QA
详见 `QA/QA-Report.md`。
src/Scripts/pro_custom_hotkeys_qa.sh
@@ -150,6 +150,17 @@
require(preferences, "StaticHotkeyInfoRow(", "internal App Grid Space shortcut must remain static")
require(preferences, "LauncherHotkeySettings.effectiveHotkey(for: .main).displayString", "Settings must display effective App Grid shortcut")
require(preferences, "LauncherHotkeySettings.effectiveHotkey(for: .quickSearch).displayString", "Settings must display effective Quick Search shortcut")
require(preferences, "HotkeyPrimaryActionButton", "custom hotkey rows must use the shared action button")
require(preferences, "ProStatusPill(text: title, style: .locked, compact: true)", "locked custom hotkey action must use the global Pro pill")
require(preferences, "if canRestore {", "restore-default action must only appear when a custom shortcut exists")
custom_row_start = preferences.find("private struct CustomizableHotkeyInfoRow")
custom_row_end = preferences.find("private struct HotkeyStatusPill")
if custom_row_start == -1 or custom_row_end == -1 or custom_row_end <= custom_row_start:
    fail("could not inspect CustomizableHotkeyInfoRow layout")
custom_row = preferences[custom_row_start:custom_row_end]
if ".buttonStyle(.borderedProminent)" in custom_row:
    fail("custom hotkey rows must not use the default blue borderedProminent Pro button")
require(pro, "case customHotkeys", "ProFeature.customHotkeys is missing")
require(pro, "case .premiumThemes, .layoutImport, .layoutExport, .unlimitedNotes, .persistentAppSorting, .customHotkeys:", "customHotkeys must be unlocked only by the Pro access state")
@@ -175,5 +186,5 @@
    if "%status%" not in data["hotkeys.error.registrationFailed"]:
        fail(f"{path.name} hotkeys.error.registrationFailed must keep %status% placeholder")
print("PASS Pro custom hotkeys QA: Pro gating, transactional Carbon registration, duplicate-safe effective shortcuts, local recording, and 29-language copy are wired")
print("PASS Pro custom hotkeys QA: Pro gating, transactional Carbon registration, duplicate-safe effective shortcuts, local recording, Pro-consistent row UI, and 29-language copy are wired")
PY