Ariver
2026-06-28 0badb998147135c75187a0c48f926b665af734c5
src/Apptag/PreferencesView.swift
@@ -3,6 +3,8 @@
// MARK: - Preferences View
private let themePreviewCountdownTimer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private enum SettingsTab: String, CaseIterable, Identifiable {
    case language
    case general
@@ -85,6 +87,7 @@
    @State private var selectedTab: SettingsTab = .general
    @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
    @State private var settingsProPromptFeature: ProFeature? = nil
    @State private var themePreviewNow = Date()
    init(initialTabRawValue: String? = nil) {
        let initialTab = initialTabRawValue.flatMap(SettingsTab.init(rawValue:)) ?? .general
@@ -610,6 +613,10 @@
    private var themeStatusSummary: String {
        if proEntitlement.isPreviewingTheme {
            if let countdown = themePreviewCountdownText {
                return tr("pro.theme.status.previewCountdown")
                    .replacingOccurrences(of: "%time%", with: countdown)
            }
            return tr("pro.theme.status.preview")
        }
        switch proEntitlement.accessState {
@@ -635,7 +642,7 @@
    private func themeOptionAccessory(for theme: AppGridTheme) -> ThemeOptionAccessory {
        if let preview = proEntitlement.themePreviewState?.theme, preview == theme {
            return .pill(text: tr("pro.card.previewBadge"), style: .preview)
            return .countdown(text: themePreviewCountdownText ?? "00:00")
        }
        if !proEntitlement.isUnlocked && theme.requiresPro {
            return .pill(text: tr("pro.card.badge"), style: .locked)
@@ -647,6 +654,24 @@
        proEntitlement.clearTransientOperationState()
        withAnimation(.spring(response: 0.22, dampingFraction: 0.84)) {
            settingsProPromptFeature = feature
        }
    }
    private var themePreviewCountdownText: String? {
        guard let preview = proEntitlement.themePreviewState else { return nil }
        return Self.formatThemePreviewCountdown(seconds: preview.remainingSeconds(at: themePreviewNow))
    }
    private static func formatThemePreviewCountdown(seconds: Int) -> String {
        let clamped = max(0, seconds)
        return String(format: "%02d:%02d", clamped / 60, clamped % 60)
    }
    private func refreshThemePreviewCountdown(now: Date = Date()) {
        themePreviewNow = now
        guard let preview = proEntitlement.themePreviewState else { return }
        if preview.remainingSeconds(at: now) <= 0 {
            proEntitlement.stopThemePreview()
        }
    }
@@ -761,6 +786,14 @@
                displayText: LauncherHotkey.quickSearch.displayString,
                statusText: hotkeyStatusText(for: .quickSearch),
                statusTone: hotkeyStatusTone(for: .quickSearch)
            )
            StaticHotkeyInfoRow(
                title: tr("trackpadGesture.title"),
                description: tr("trackpadGesture.description"),
                displayText: "taglauncher://show",
                statusText: tr("trackpadGesture.status"),
                statusTone: .neutral
            )
        }
    }
@@ -1473,6 +1506,10 @@
        .onAppear {
            syncSelectedLanguage()
            showPendingHotkeyWarningIfNeeded()
            refreshThemePreviewCountdown()
        }
        .onReceive(themePreviewCountdownTimer) { now in
            refreshThemePreviewCountdown(now: now)
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in
            showPendingHotkeyWarningIfNeeded()
@@ -1662,6 +1699,7 @@
private enum ThemeOptionAccessory {
    case checkmark
    case circle
    case countdown(text: String)
    case pill(text: String, style: ProStatusPillStyle)
}
@@ -1739,6 +1777,22 @@
            Image(systemName: "circle")
                .font(.system(size: 16, weight: .semibold))
                .foregroundStyle(Color.secondary.opacity(0.42))
        case .countdown(let text):
            Text(text)
                .font(.system(size: 10, weight: .semibold, design: .monospaced))
                .foregroundStyle(Color(red: 0.30, green: 0.22, blue: 0.08))
                .lineLimit(1)
                .frame(minWidth: 42)
                .padding(.horizontal, 8)
                .padding(.vertical, 4)
                .background(
                    Capsule(style: .continuous)
                        .fill(Color.orange.opacity(0.14))
                )
                .overlay(
                    Capsule(style: .continuous)
                        .stroke(Color.orange.opacity(0.28), lineWidth: 1)
                )
        case .pill(let text, let style):
            ProStatusPill(text: text, style: style, compact: true)
        }