From 18c90acda54cb46815a081ecfc36ea8f2b9e93fd Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 29 Jun 2026 00:12:04 +0800
Subject: [PATCH] Fix custom hotkey settings UI for 8.2.1

---
 src/Release/AppStore-8.2.1-20260628.2354/QA/QA-Report.md                                  |   61 ++++++++++++
 src/Release/AppStore-8.2.1-20260628.2354/RELEASE_MANIFEST.md                              |   25 +++++
 src/Apptag/PreferencesView.swift                                                          |  170 +++++++++++++++++++++++-----------
 src/CHANGELOG.md                                                                          |    8 +
 src/Release/AppStore-8.2.1-20260628.2354/Archive/SHA256SUMS.txt                           |    1 
 src/Apptag/Info.plist                                                                     |    4 
 src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg |    0 
 src/Scripts/pro_custom_hotkeys_qa.sh                                                      |   13 ++
 8 files changed, 224 insertions(+), 58 deletions(-)

diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 6435f10..0d72ecf 100644
--- a/src/Apptag/Info.plist
+++ b/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>
diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 41fec66..8838d3d 100644
--- a/src/Apptag/PreferencesView.swift
+++ b/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,32 +1730,27 @@
     let statusTone: HotkeyStatusTone
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 12) {
-            HStack(alignment: .top, spacing: 12) {
-                VStack(alignment: .leading, spacing: 3) {
-                    Text(title)
-                        .font(.system(size: 13, weight: .semibold))
-                    Text(description)
-                        .font(.caption)
-                        .foregroundStyle(.secondary)
-                        .fixedSize(horizontal: false, vertical: true)
-                }
-                .frame(maxWidth: .infinity, alignment: .leading)
-
-                VStack(alignment: .leading, spacing: 4) {
-                    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: 180, alignment: .leading)
+        HStack(alignment: .center, spacing: 18) {
+            VStack(alignment: .leading, spacing: 3) {
+                Text(title)
+                    .font(.system(size: 13, weight: .semibold))
+                Text(description)
+                    .font(.caption)
+                    .foregroundStyle(.secondary)
+                    .fixedSize(horizontal: false, vertical: true)
             }
+            .frame(maxWidth: .infinity, alignment: .leading)
+
+            HStack(alignment: .center, spacing: 12) {
+                Text(displayText)
+                    .font(.system(size: 15, weight: .semibold))
+                    .lineLimit(1)
+                    .minimumScaleFactor(0.85)
+                    .frame(width: 156, alignment: .leading)
+
+                HotkeyStatusPill(text: statusText, tone: statusTone)
+            }
+            .frame(width: 310, alignment: .leading)
         }
         .padding(12)
         .background(
@@ -1776,36 +1773,32 @@
     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) {
-                VStack(alignment: .leading, spacing: 3) {
-                    Text(title)
-                        .font(.system(size: 13, weight: .semibold))
-                    Text(description)
-                        .font(.caption)
-                        .foregroundStyle(.secondary)
-                        .fixedSize(horizontal: false, vertical: true)
-                }
-                .frame(maxWidth: .infinity, alignment: .leading)
+        HStack(alignment: .center, spacing: 18) {
+            VStack(alignment: .leading, spacing: 3) {
+                Text(title)
+                    .font(.system(size: 13, weight: .semibold))
+                Text(description)
+                    .font(.caption)
+                    .foregroundStyle(.secondary)
+                    .fixedSize(horizontal: false, vertical: true)
+            }
+            .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)
                         }
                     }
-
-                    HStack(spacing: 8) {
-                        Button(customizeTitle, action: onCustomize)
-                            .buttonStyle(.borderedProminent)
-                            .controlSize(.small)
-
-                        Button(restoreTitle, action: onRestore)
-                            .buttonStyle(.bordered)
-                            .controlSize(.small)
-                            .disabled(!canRestore)
-                    }
                 }
-                .frame(width: 230, alignment: .leading)
+                .frame(width: 156, alignment: .leading)
+
+                HotkeyPrimaryActionButton(
+                    title: customizeTitle,
+                    isProLocked: isProLocked,
+                    action: onCustomize
+                )
+
+                if canRestore {
+                    HotkeySecondaryActionButton(
+                        title: restoreTitle,
+                        action: onRestore
+                    )
+                }
             }
+            .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
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index dadea7f..9aee5ca 100644
--- a/src/CHANGELOG.md
+++ b/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 全局唤起快捷键
diff --git a/src/Release/AppStore-8.2.1-20260628.2354/Archive/SHA256SUMS.txt b/src/Release/AppStore-8.2.1-20260628.2354/Archive/SHA256SUMS.txt
new file mode 100644
index 0000000..49a578a
--- /dev/null
+++ b/src/Release/AppStore-8.2.1-20260628.2354/Archive/SHA256SUMS.txt
@@ -0,0 +1 @@
+f166743d18af08225056a77bbc61dd39c5ee4bd264273786d094bf941b18c271  src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg
diff --git a/src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg b/src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg
new file mode 100644
index 0000000..9c04c34
--- /dev/null
+++ b/src/Release/AppStore-8.2.1-20260628.2354/Archive/TagLauncher-8.2.1-build20260628.2354.dmg
Binary files differ
diff --git a/src/Release/AppStore-8.2.1-20260628.2354/QA/QA-Report.md b/src/Release/AppStore-8.2.1-20260628.2354/QA/QA-Report.md
new file mode 100644
index 0000000..d71b471
--- /dev/null
+++ b/src/Release/AppStore-8.2.1-20260628.2354/QA/QA-Report.md
@@ -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`
diff --git a/src/Release/AppStore-8.2.1-20260628.2354/RELEASE_MANIFEST.md b/src/Release/AppStore-8.2.1-20260628.2354/RELEASE_MANIFEST.md
new file mode 100644
index 0000000..f862a84
--- /dev/null
+++ b/src/Release/AppStore-8.2.1-20260628.2354/RELEASE_MANIFEST.md
@@ -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`。
diff --git a/src/Scripts/pro_custom_hotkeys_qa.sh b/src/Scripts/pro_custom_hotkeys_qa.sh
index d725620..bb797a4 100755
--- a/src/Scripts/pro_custom_hotkeys_qa.sh
+++ b/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

--
Gitblit v1.9.3