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