Ariver
2026-06-28 bd9ad3573731707537b9aff037d2240bf133303a
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
@@ -578,7 +581,7 @@
        }
        switch proEntitlement.accessState {
        case .free:
            return .locked
            return .neutral
        case .purchased:
            return .unlocked
        case .legacy:
@@ -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()
        }
    }
@@ -762,6 +787,14 @@
                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
            )
        }
    }
@@ -803,7 +836,7 @@
        )
    }
    private var dataProStatusCard: some View {
    private var proStatusCard: some View {
        HStack(alignment: .top, spacing: 18) {
            VStack(alignment: .leading, spacing: 10) {
                HStack(spacing: 10) {
@@ -1139,8 +1172,6 @@
                Spacer(minLength: 36)
                VStack(alignment: .center, spacing: 24) {
                    dataProStatusCard
                    dataSection(minHeight: 124) {
                        HStack(alignment: .center, spacing: 12) {
                            dataSectionTitle(tr("settings.initialLayout"))
@@ -1291,62 +1322,67 @@
                    case .about:
            // Tab 6: About
            VStack(spacing: 0) {
                Spacer(minLength: 72)
                Spacer(minLength: 44)
                HStack {
                    if let icon = NSImage(named: NSImage.applicationIconName) {
                        Image(nsImage: icon)
                            .resizable()
                            .frame(width: 128, height: 128)
                    }
                    VStack(alignment: .leading, spacing: 4) {
                        Text("TagLauncher")
                            .font(.title2)
                            .fontWeight(.semibold)
                        Text(tr("app.description"))
                            .font(.body)
                            .foregroundStyle(.secondary)
                        Text("\(tr("app.version")) \(appVersion) (\(tr("app.build")) \(buildVersion))")
                            .font(.callout)
                            .foregroundStyle(.tertiary)
                VStack(alignment: .center, spacing: 24) {
                    proStatusCard
                        Divider()
                            .padding(.vertical, 4)
                        HStack(spacing: 8) {
                            Button {
                                NSWorkspace.shared.open(helpPDFURL)
                            } label: {
                                Label(tr("help.openPDF"), systemImage: "questionmark.circle")
                            }
                            .buttonStyle(.borderedProminent)
                            .controlSize(.regular)
                            Button {
                                NSPasteboard.general.clearContents()
                                NSPasteboard.general.setString(helpPDFURL.absoluteString, forType: .string)
                            } label: {
                                Image(systemName: "link")
                            }
                            .buttonStyle(.bordered)
                            .controlSize(.regular)
                            .help(tr("help.copyLink"))
                    HStack(alignment: .center, spacing: 24) {
                        if let icon = NSImage(named: NSImage.applicationIconName) {
                            Image(nsImage: icon)
                                .resizable()
                                .frame(width: 112, height: 112)
                        }
                        VStack(alignment: .leading, spacing: 4) {
                            Text("TagLauncher")
                                .font(.title2)
                                .fontWeight(.semibold)
                            Text(tr("app.description"))
                                .font(.body)
                                .foregroundStyle(.secondary)
                            Text("\(tr("app.version")) \(appVersion) (\(tr("app.build")) \(buildVersion))")
                                .font(.callout)
                                .foregroundStyle(.tertiary)
                        Text(tr("help.currentLanguage"))
                            .font(.caption)
                            .foregroundStyle(.secondary)
                            .padding(.bottom, 2)
                            Divider()
                                .padding(.vertical, 4)
                        Text("万物之中,希望最美")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                        Text("永桔@shanghai3168@gmail.com")
                            .font(.caption)
                            .foregroundStyle(.secondary)
                            HStack(spacing: 8) {
                                Button {
                                    NSWorkspace.shared.open(helpPDFURL)
                                } label: {
                                    Label(tr("help.openPDF"), systemImage: "questionmark.circle")
                                }
                                .buttonStyle(.borderedProminent)
                                .controlSize(.regular)
                                Button {
                                    NSPasteboard.general.clearContents()
                                    NSPasteboard.general.setString(helpPDFURL.absoluteString, forType: .string)
                                } label: {
                                    Image(systemName: "link")
                                }
                                .buttonStyle(.bordered)
                                .controlSize(.regular)
                                .help(tr("help.copyLink"))
                            }
                            Text(tr("help.currentLanguage"))
                                .font(.caption)
                                .foregroundStyle(.secondary)
                                .padding(.bottom, 2)
                            Text("万物之中,希望最美")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                            Text("永桔@shanghai3168@gmail.com")
                                .font(.caption)
                                .foregroundStyle(.secondary)
                        }
                    }
                    .frame(width: dataPanelWidth, alignment: .leading)
                }
                .frame(width: 560, alignment: .leading)
                .frame(width: dataPanelWidth, alignment: .center)
                Spacer(minLength: 30)
                Spacer(minLength: 18)
@@ -1470,6 +1506,10 @@
        .onAppear {
            syncSelectedLanguage()
            showPendingHotkeyWarningIfNeeded()
            refreshThemePreviewCountdown()
        }
        .onReceive(themePreviewCountdownTimer) { now in
            refreshThemePreviewCountdown(now: now)
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in
            showPendingHotkeyWarningIfNeeded()
@@ -1659,6 +1699,7 @@
private enum ThemeOptionAccessory {
    case checkmark
    case circle
    case countdown(text: String)
    case pill(text: String, style: ProStatusPillStyle)
}
@@ -1736,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)
        }