From cf4a1ad4dbc8f4a3f61a0a1a32f63d31c3562b27 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 03:07:40 +0800
Subject: [PATCH] Record TagLauncher 8.3.2 release freeze

---
 src/Apptag/PreferencesView.swift |  324 ++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 278 insertions(+), 46 deletions(-)

diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 0e6c0e5..a147190 100644
--- a/src/Apptag/PreferencesView.swift
+++ b/src/Apptag/PreferencesView.swift
@@ -54,6 +54,7 @@
     private let settingsWindowWidth: CGFloat = 1000
     private let settingsWindowHeight: CGFloat = 640
     private let settingsContentWidth: CGFloat = 940
+    private let languageContentWidth: CGFloat = 820
     private let generalContentWidth: CGFloat = 720
     private let generalLabelWidth: CGFloat = 190
     private let generalControlWidth: CGFloat = 500
@@ -93,10 +94,13 @@
     @State private var hotkeyStatusToastToken: UUID? = nil
     @State private var recordingHotkeyKind: LauncherHotkeyKind? = nil
     @State private var hotkeyRecorderMonitor: Any? = nil
+    @State private var settingsEscapeMonitor: Any? = nil
     @State private var hotkeySettingsRefreshToken = UUID()
     @State private var selectedTab: SettingsTab = .general
     @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
     @State private var settingsProPromptFeature: ProFeature? = nil
+    @State private var settingsCustomContainerQuotaStatus: ProCustomContainerQuotaStatus? = nil
+    @State private var settingsCustomContainerQuotaOverviewStatus = ProEntitlementPolicy.customContainerQuotaStatus()
     @State private var themePreviewNow = Date()
 
     init(initialTabRawValue: String? = nil) {
@@ -116,11 +120,13 @@
             apps = TagEditor.annotate(apps: apps)
             let colors = store.tags.mapValues { $0.color }
             let customColors = store.tags.compactMapValues { $0.customColor }
+            let quotaStatus = ProEntitlementPolicy.customContainerQuotaStatus(in: store)
             DispatchQueue.main.async {
                 allApps = apps
                 tagColors = colors
                 tagCustomColors = customColors
                 categoryScheme = store.categoryScheme
+                settingsCustomContainerQuotaOverviewStatus = quotaStatus
             }
         }
     }
@@ -230,6 +236,68 @@
         }
     }
 
+    private func installSettingsEscapeMonitor() {
+        guard settingsEscapeMonitor == nil else { return }
+        settingsEscapeMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in
+            guard Int(event.keyCode) == kVK_Escape,
+                  isSettingsOverlayEscapeEvent(event)
+            else { return event }
+            return handleSettingsEscapeKey() ? nil : event
+        }
+    }
+
+    private func removeSettingsEscapeMonitor() {
+        if let settingsEscapeMonitor {
+            NSEvent.removeMonitor(settingsEscapeMonitor)
+        }
+        settingsEscapeMonitor = nil
+    }
+
+    private func isSettingsOverlayEscapeEvent(_ event: NSEvent) -> Bool {
+        guard let window = preferencesWindow,
+              window.isVisible,
+              let parent = window.parent as? OverlayPanel,
+              parent.isVisible
+        else { return false }
+
+        if let eventWindow = event.window {
+            return eventWindow == window || eventWindow == parent || eventWindow.parent == window
+        }
+        return NSApp.keyWindow == window || NSApp.keyWindow == parent || NSApp.mainWindow == window
+    }
+
+    private func handleSettingsEscapeKey() -> Bool {
+        guard let window = preferencesWindow else { return false }
+        guard !isDataFilePanelPresented, window.attachedSheet == nil else { return false }
+
+        if settingsProPromptFeature != nil {
+            dismissSettingsProPrompt()
+            return true
+        }
+
+        if showApplySystemSchemeConfirmation {
+            withAnimation(.easeOut(duration: 0.14)) {
+                showApplySystemSchemeConfirmation = false
+            }
+            return true
+        }
+
+        if showResetToUncategorizedConfirmation {
+            withAnimation(.easeOut(duration: 0.14)) {
+                showResetToUncategorizedConfirmation = false
+            }
+            return true
+        }
+
+        if recordingHotkeyKind != nil {
+            stopHotkeyRecording(showCancelledToast: true)
+            return true
+        }
+
+        window.performClose(nil)
+        return true
+    }
+
     private var preferencesWindow: NSWindow? {
         if let taggedWindow = NSApp.windows.first(where: {
             $0.identifier?.rawValue == "TagLauncherPreferencesWindow" && $0.isVisible
@@ -242,14 +310,21 @@
             .first(where: isPreferencesWindowFallback(_:))
     }
 
+    private var localizedPreferencesWindowTitle: String {
+        tr("menu.preferences").replacingOccurrences(of: "…", with: "")
+    }
+
+    private func refreshPreferencesWindowTitle() {
+        preferencesWindow?.title = localizedPreferencesWindowTitle
+    }
+
     private func isPreferencesWindowFallback(_ window: NSWindow) -> Bool {
         guard window.isVisible,
               !(window is NSPanel),
               !(window is OverlayPanel)
         else { return false }
 
-        let preferencesTitle = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
-        return window.title == preferencesTitle
+        return window.title == localizedPreferencesWindowTitle
     }
 
     private func showDataAlert(title: String, message: String) {
@@ -688,6 +763,23 @@
         Color(red: 0.56, green: 0.35, blue: 0.03)
     }
 
+    @ViewBuilder
+    private func proHeaderStatusLabel(compact: Bool = false) -> some View {
+        if proEntitlement.accessState == .free && !proEntitlement.isPreviewingProAppearance {
+            Text(tr("settings.proStatus.freeUser"))
+                .font(.system(size: compact ? 12 : 13, weight: .semibold))
+                .foregroundStyle(.secondary)
+                .lineLimit(1)
+                .minimumScaleFactor(0.82)
+        } else {
+            ProStatusPill(
+                text: tr(proEntitlement.compactStatusTextKey),
+                style: proStatusPillStyle,
+                compact: compact
+            )
+        }
+    }
+
     private func centeredSettingsScrollContent<Content: View>(
         width: CGFloat,
         @ViewBuilder content: @escaping () -> Content
@@ -733,10 +825,7 @@
         compact: Bool = false
     ) -> some View {
         HStack(spacing: compact ? 8 : 12) {
-            ProStatusPill(
-                text: tr(proEntitlement.compactStatusTextKey),
-                style: proStatusPillStyle
-            )
+            proHeaderStatusLabel(compact: compact)
             .layoutPriority(1)
 
             Text(tr(textKey))
@@ -811,7 +900,9 @@
         }
 
         switch selectedTab {
-        case .general, .language, .about:
+        case .language, .about:
+            return ""
+        case .general:
             return tr(proEntitlement.isUnlocked ? "settings.proBenefit.general" : "settings.proGuide.general")
         case .theme:
             return themeStatusSummary
@@ -826,44 +917,105 @@
         }
     }
 
+    private var effectiveCustomContainerQuotaStatus: ProCustomContainerQuotaStatus {
+        if proEntitlement.isUnlocked {
+            return ProCustomContainerQuotaStatus(isUnlimited: true, used: 0, limit: Int.max)
+        }
+        return settingsCustomContainerQuotaOverviewStatus
+    }
+
+    private var customContainerQuotaAccessory: some View {
+        let status = effectiveCustomContainerQuotaStatus
+        let text = status.isUnlimited
+            ? tr("pro.customContainers.quotaUnlimitedShort")
+            : tr(
+                "pro.customContainers.quotaShort",
+                replacements: [
+                    "%used%": "\(status.used)",
+                    "%limit%": "\(status.limit)"
+                ]
+            )
+        return Text(text)
+            .font(.system(size: 12, weight: .medium))
+            .foregroundStyle(.secondary)
+            .lineLimit(1)
+    }
+
+    private func refreshSettingsCustomContainerQuotaStatus(_ status: ProCustomContainerQuotaStatus) {
+        settingsCustomContainerQuotaOverviewStatus = status
+    }
+
+    private func refreshSettingsCustomContainerQuotaStatus(in store: TagDatabase.Store? = nil) {
+        settingsCustomContainerQuotaOverviewStatus = ProEntitlementPolicy.customContainerQuotaStatus(in: store)
+    }
+
     private var shouldShowSettingsFixedProHeader: Bool {
-        selectedTab != .language && selectedTab != .about
+        true
+    }
+
+    private var shouldCenterUnlockedIdentityHeader: Bool {
+        guard proEntitlement.isUnlocked else { return false }
+        switch selectedTab {
+        case .language, .pro, .about:
+            return true
+        case .general, .theme, .hotkeys, .tags, .data:
+            return false
+        }
     }
 
     private var settingsFixedProHeader: some View {
         HStack {
             HStack(spacing: 12) {
-                if selectedTab != .pro {
+                if shouldCenterUnlockedIdentityHeader {
+                    Spacer(minLength: 0)
+
                     ProStatusPill(
-                        text: tr(proEntitlement.compactStatusTextKey),
-                        style: proStatusPillStyle
+                        text: tr("pro.status.unlocked"),
+                        style: .unlocked,
+                        compact: true
                     )
                     .layoutPriority(1)
-                }
 
-                Text(settingsProHeaderText)
-                    .font(.system(size: 13, weight: .medium))
-                    .foregroundStyle(.secondary)
-                    .lineLimit(2)
-                    .fixedSize(horizontal: false, vertical: true)
-                    .layoutPriority(2)
+                    Text(tr("settings.proStatus.proUser"))
+                        .font(.system(size: 13, weight: .medium))
+                        .foregroundStyle(.secondary)
+                        .lineLimit(1)
+                        .fixedSize(horizontal: false, vertical: true)
+                        .layoutPriority(2)
 
-                Spacer(minLength: 8)
-
-                if !proEntitlement.isUnlocked {
-                    Button(unlockButtonTitle()) {
-                        presentSettingsProPrompt(for: settingsProHeaderFeature)
+                    Spacer(minLength: 0)
+                } else {
+                    if selectedTab != .pro {
+                        proHeaderStatusLabel(compact: true)
+                        .layoutPriority(1)
                     }
-                    .buttonStyle(.borderedProminent)
-                    .controlSize(.small)
-                    .layoutPriority(3)
 
-                    Button(tr("pro.card.restore")) {
-                        proEntitlement.restorePurchases()
+                    if !settingsProHeaderText.isEmpty {
+                        Text(settingsProHeaderText)
+                            .font(.system(size: 13, weight: .medium))
+                            .foregroundStyle(.secondary)
+                            .lineLimit(2)
+                            .fixedSize(horizontal: false, vertical: true)
+                            .layoutPriority(2)
                     }
-                    .buttonStyle(.bordered)
-                    .controlSize(.small)
-                    .layoutPriority(3)
+
+                    Spacer(minLength: 8)
+
+                    if !proEntitlement.isUnlocked {
+                        Button(unlockButtonTitle()) {
+                            presentSettingsProPrompt(for: settingsProHeaderFeature)
+                        }
+                        .buttonStyle(.borderedProminent)
+                        .controlSize(.small)
+                        .layoutPriority(3)
+
+                        Button(tr("pro.card.restore")) {
+                            proEntitlement.restorePurchases()
+                        }
+                        .buttonStyle(.bordered)
+                        .controlSize(.small)
+                        .layoutPriority(3)
+                    }
                 }
             }
             .padding(.horizontal, 16)
@@ -887,6 +1039,33 @@
 
     private var proTabComparisonFeatures: [ProTabComparisonFeature] {
         [
+            ProTabComparisonFeature(
+                title: tr("pro.feature.maintainableTagCount"),
+                freeStatusKey: "settings.proStatus.maxSixTags",
+                proLockedStatusKey: "settings.proStatus.unlimitedTags",
+                proUnlockedStatusKey: "settings.proStatus.unlimitedTags",
+                freeStyle: .neutral,
+                proLockedStyle: .neutral,
+                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",
+                proUnlockedStatusKey: "settings.proStatus.supported",
+                freeStyle: .neutral,
+                proLockedStyle: .neutral,
+                proUnlockedStyle: .neutral
+            ),
             ProTabComparisonFeature(
                 title: tr("pro.feature.customTagColors"),
                 freeStatusKey: "settings.proStatus.unavailableFeature",
@@ -953,7 +1132,7 @@
                     Text(tr("settings.proCompare.free"))
                         .font(.system(size: 12, weight: .semibold))
                         .foregroundStyle(.secondary)
-                        .frame(width: 118, alignment: .center)
+                        .frame(width: 118, alignment: .leading)
                     HStack(spacing: 4) {
                         Image(systemName: "crown")
                             .font(.system(size: 12, weight: .semibold))
@@ -963,7 +1142,7 @@
                     .foregroundStyle(proAccentColor)
                     .lineLimit(1)
                     .minimumScaleFactor(0.82)
-                    .frame(width: 118, alignment: .center)
+                    .frame(width: 118, alignment: .leading)
                 }
 
                 ForEach(proTabComparisonFeatures) { feature in
@@ -983,14 +1162,14 @@
                             style: feature.freeStyle,
                             compact: true
                         )
-                        .frame(width: 118, alignment: .center)
+                        .frame(width: 118, alignment: .leading)
 
                         ProStatusPill(
                             text: tr(proStatusKey),
                             style: proStyle,
                             compact: true
                         )
-                        .frame(width: 118, alignment: .center)
+                        .frame(width: 118, alignment: .leading)
                     }
                     .padding(.horizontal, 10)
                     .padding(.vertical, 8)
@@ -1124,11 +1303,22 @@
         return effectiveDisplayMode == mode ? .checkmark : .circle
     }
 
-    private func presentSettingsProPrompt(for feature: ProFeature) {
+    private func presentSettingsProPrompt(
+        for feature: ProFeature,
+        customContainerQuotaStatus: ProCustomContainerQuotaStatus? = nil
+    ) {
         proEntitlement.clearTransientOperationState()
+        settingsCustomContainerQuotaStatus = customContainerQuotaStatus
         withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
             settingsProPromptFeature = feature
         }
+    }
+
+    private func presentSettingsCustomContainerQuotaPrompt(_ status: ProCustomContainerQuotaStatus) {
+        presentSettingsProPrompt(
+            for: .customContainerQuota,
+            customContainerQuotaStatus: status
+        )
     }
 
     private var themePreviewCountdownText: String? {
@@ -1163,6 +1353,38 @@
         withAnimation(.easeOut(duration: 0.18)) {
             settingsProPromptFeature = nil
         }
+        settingsCustomContainerQuotaStatus = nil
+    }
+
+    private func settingsProPromptMessage(for feature: ProFeature) -> String {
+        if feature == .customContainerQuota,
+           let status = settingsCustomContainerQuotaStatus,
+           !status.isUnlimited {
+            return tr(
+                "pro.customContainers.quotaPrompt",
+                replacements: [
+                    "%used%": "\(status.used)",
+                    "%limit%": "\(status.limit)",
+                    "%remaining%": "\(status.remaining)"
+                ]
+            )
+        }
+        return tr(feature.promptMessageKey)
+    }
+
+    private func settingsProPromptSecondaryTitle(for feature: ProFeature) -> String {
+        feature == .customContainerQuota
+            ? tr("pro.customContainers.manageExisting")
+            : tr("pro.card.restore")
+    }
+
+    private func handleSettingsProPromptSecondaryAction(for feature: ProFeature) {
+        if feature == .customContainerQuota {
+            dismissSettingsProPrompt()
+            selectedTab = .tags
+            return
+        }
+        proEntitlement.restorePurchases()
     }
 
     private func handleProAccessStateChange(_ newState: ProAccessState) {
@@ -1354,10 +1576,7 @@
             proUnlockedBenefitLine("pro.theme.status.unlocked", bottomPadding: 0)
         } else {
             HStack(spacing: 12) {
-                ProStatusPill(
-                    text: tr(proEntitlement.compactStatusTextKey),
-                    style: proStatusPillStyle
-                )
+                proHeaderStatusLabel()
 
                 Text(themeStatusSummary)
                     .font(.system(size: 13, weight: .medium))
@@ -1445,7 +1664,7 @@
                     switch selectedTab {
                     case .language:
                 // Tab 1: Language
-                centeredSettingsScrollContent(width: settingsContentWidth) {
+                centeredSettingsScrollContent(width: languageContentWidth) {
                         LazyVGrid(columns: languageColumns, alignment: .leading, spacing: 6) {
                             Button {
                                 selectedLanguage = L10n.automaticCode
@@ -1638,10 +1857,19 @@
                     excludedTagNames: ["Mac自带", defaultGroupName],
                     isCustomColorUnlocked: proEntitlement.isUnlocked,
                     onLockedCustomColor: { presentSettingsProPrompt(for: .customTagColors) },
-                    onRefresh: { scanApps() }
+                    onCustomContainerQuotaExceeded: presentSettingsCustomContainerQuotaPrompt,
+                    onCustomContainerQuotaStatusChanged: refreshSettingsCustomContainerQuotaStatus,
+                    topLeadingAccessory: AnyView(customContainerQuotaAccessory),
+                    onRefresh: {
+                        refreshSettingsCustomContainerQuotaStatus()
+                        scanApps()
+                    }
                 )
             }
-            .onAppear { scanApps() }
+            .onAppear {
+                refreshSettingsCustomContainerQuotaStatus()
+                scanApps()
+            }
 
                     case .data:
             // Tab 5: Data
@@ -1866,6 +2094,7 @@
                 } else {
                     syncSelectedLanguage()
                 }
+                refreshPreferencesWindowTitle()
                 scanApps()
                 refreshDataState()
                 showLanguageRefresh()
@@ -1935,15 +2164,15 @@
 
                     ProUpgradePromptView(
                         title: tr(feature.titleKey),
-                        message: tr(feature.promptMessageKey),
+                        message: settingsProPromptMessage(for: feature),
                         benefitText: tr(feature.benefitKey),
                         operationText: proEntitlement.operationState.statusMessageKey.map(tr),
                         unlockTitle: unlockButtonTitle(),
-                        restoreTitle: tr("pro.card.restore"),
+                        restoreTitle: settingsProPromptSecondaryTitle(for: feature),
                         isBusy: proEntitlement.operationState.isBusy,
                         onClose: dismissSettingsProPrompt,
                         onUnlock: { proEntitlement.purchasePro() },
-                        onRestore: { proEntitlement.restorePurchases() }
+                        onRestore: { handleSettingsProPromptSecondaryAction(for: feature) }
                     )
                 }
                 .zIndex(4)
@@ -1975,10 +2204,13 @@
         .frame(width: settingsWindowWidth, height: settingsWindowHeight)
         .onAppear {
             syncSelectedLanguage()
+            refreshPreferencesWindowTitle()
+            installSettingsEscapeMonitor()
             showPendingHotkeyWarningIfNeeded()
             refreshThemePreviewCountdown()
         }
         .onDisappear {
+            removeSettingsEscapeMonitor()
             stopHotkeyRecording()
         }
         .onReceive(themePreviewCountdownTimer) { now in

--
Gitblit v1.9.3