From 2309264ee94daca4b9abe3592d84602c98b77690 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 02 Jul 2026 11:16:52 +0800
Subject: [PATCH] Freeze TagLauncher 8.3.5 release artifacts

---
 src/Apptag/PreferencesView.swift |  199 +++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 190 insertions(+), 9 deletions(-)

diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 67f2899..32a95e1 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) {
@@ -842,6 +917,38 @@
         }
     }
 
+    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 {
         true
     }
@@ -932,6 +1039,24 @@
 
     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.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",
@@ -1169,11 +1294,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? {
@@ -1208,6 +1344,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) {
@@ -1487,7 +1655,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
@@ -1680,10 +1848,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
@@ -1908,6 +2085,7 @@
                 } else {
                     syncSelectedLanguage()
                 }
+                refreshPreferencesWindowTitle()
                 scanApps()
                 refreshDataState()
                 showLanguageRefresh()
@@ -1977,15 +2155,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)
@@ -2017,10 +2195,13 @@
         .frame(width: settingsWindowWidth, height: settingsWindowHeight)
         .onAppear {
             syncSelectedLanguage()
+            refreshPreferencesWindowTitle()
+            installSettingsEscapeMonitor()
             showPendingHotkeyWarningIfNeeded()
             refreshThemePreviewCountdown()
         }
         .onDisappear {
+            removeSettingsEscapeMonitor()
             stopHotkeyRecording()
         }
         .onReceive(themePreviewCountdownTimer) { now in

--
Gitblit v1.9.3