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 | 208 +++++++++++++++++++++++++++++++++++++++++++++++++--
1 files changed, 199 insertions(+), 9 deletions(-)
diff --git a/src/Apptag/PreferencesView.swift b/src/Apptag/PreferencesView.swift
index 67f2899..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) {
@@ -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,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",
@@ -1169,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? {
@@ -1208,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) {
@@ -1487,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
@@ -1680,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
@@ -1908,6 +2094,7 @@
} else {
syncSelectedLanguage()
}
+ refreshPreferencesWindowTitle()
scanApps()
refreshDataState()
showLanguageRefresh()
@@ -1977,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)
@@ -2017,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