From 9c317d89a39d0871f15f8282aec6c7f3dde7768c Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Fri, 26 Jun 2026 22:42:44 +0800
Subject: [PATCH] Polish Pro status UI and app grid layout

---
 src/Apptag/ApptagApp.swift |  162 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 156 insertions(+), 6 deletions(-)

diff --git a/src/Apptag/ApptagApp.swift b/src/Apptag/ApptagApp.swift
index beb61cb..bb6b32b 100644
--- a/src/Apptag/ApptagApp.swift
+++ b/src/Apptag/ApptagApp.swift
@@ -34,6 +34,7 @@
     private static let statusItemButtonIdentifier = NSUserInterfaceItemIdentifier("TagLauncherStatusItemButton")
     private static let statusItemAccessibilityLabel = AppIdentity.displayName
     private static let showAppListMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherShowAppListMenuItem")
+    private static let proStatusMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherProStatusMenuItem")
     private static let helpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherHelpMenu")
     private static let downloadHelpMenuItemIdentifier = NSUserInterfaceItemIdentifier("TagLauncherDownloadHelpMenuItem")
     private static let externalActivationNotification = Notification.Name("TagLauncherExternalActivationRequested")
@@ -193,8 +194,10 @@
     func applicationDidFinishLaunching(_ notification: Notification) {
         AppDefaults.register()
         L10n.setup()
+        ProEntitlementCenter.shared.start()
         migrateDefaultGroupName()
         TagDatabase.seedDefaultTags()
+        _ = TagDatabase.relocalizeSystemTagsForCurrentLanguage()
         syncChromeSettings(force: true)
         observeHotkeyStatusChanges()
         registerConfiguredHotkeys()
@@ -342,11 +345,53 @@
         }
     }
 
+    private func foregroundWindowForActivationPolicyRestore() -> NSWindow? {
+        if let settingsWindow, settingsWindow.isVisible {
+            return settingsWindow
+        }
+        if let overlayWindow, overlayWindow.isVisible {
+            return overlayWindow
+        }
+        return NSApp.keyWindow
+    }
+
+    private func setLauncherActivationPolicy(_ desiredPolicy: NSApplication.ActivationPolicy) {
+        guard NSApp.activationPolicy() != desiredPolicy else { return }
+
+        let restoreWindow = desiredPolicy == .accessory && NSApp.isActive
+            ? foregroundWindowForActivationPolicyRestore()
+            : nil
+
+        // Deactivate before switching regular -> accessory; otherwise Dock can
+        // keep a stale running tile until Dock itself restarts.
+        if desiredPolicy == .accessory && NSApp.isActive {
+            NSApp.deactivate()
+        }
+
+        NSApp.setActivationPolicy(desiredPolicy)
+
+        guard desiredPolicy == .accessory,
+              let restoreWindow,
+              restoreWindow.isVisible
+        else { return }
+
+        DispatchQueue.main.async { [weak self, weak restoreWindow] in
+            guard let self,
+                  let restoreWindow,
+                  restoreWindow.isVisible,
+                  self.requiresForegroundOwnership
+            else { return }
+            NSApp.activate(ignoringOtherApps: true)
+            restoreWindow.makeKeyAndOrderFront(nil)
+            restoreWindow.orderFrontRegardless()
+        }
+    }
+
     private func beginLauncherForegroundOwnership(activate: Bool = true, keyWindow: NSWindow? = nil) {
         let showDock = UserDefaults.standard.bool(forKey: Self.showDockIconKey)
         let desiredPolicy: NSApplication.ActivationPolicy = showDock ? .regular : .accessory
         if NSApp.activationPolicy() != desiredPolicy {
-            NSApp.setActivationPolicy(desiredPolicy)
+            setLauncherActivationPolicy(desiredPolicy)
         }
         if activate, showDock {
             claimLauncherForeground(keyWindow: keyWindow)
@@ -363,7 +408,7 @@
         overlayGeneration expectedOverlayGeneration: Int? = nil
     ) {
         if NSApp.activationPolicy() != .regular {
-            NSApp.setActivationPolicy(.regular)
+            setLauncherActivationPolicy(.regular)
         }
 
         if isOverlayVisible && !NSApp.presentationOptions.contains(.hideDock) {
@@ -425,7 +470,7 @@
             ? .regular
             : (showDock ? .regular : .accessory))))
         if NSApp.activationPolicy() != desiredPolicy {
-            NSApp.setActivationPolicy(desiredPolicy)
+            setLauncherActivationPolicy(desiredPolicy)
         }
 
         let desiredPresentation: NSApplication.PresentationOptions = isOverlayVisible ? [.hideDock] : []
@@ -617,6 +662,9 @@
         versionItem.isEnabled = false
         menu.addItem(versionItem)
 
+        let proStatusItem = makeProStatusMenuItem()
+        menu.addItem(proStatusItem)
+
         menu.addItem(.separator())
         let prefsItem = NSMenuItem(
             title: tr("menu.preferences"),
@@ -653,6 +701,7 @@
     }
 
     func menuWillOpen(_ menu: NSMenu) {
+        configureProStatusMenuItem(in: menu)
         statusMenuScreenForNextOverlay = overlayController.screenContainingCurrentPointer()
             ?? statusItem?.button?.window?.screen
             ?? NSScreen.main
@@ -722,6 +771,53 @@
 
         image.isTemplate = true
         image.accessibilityDescription = "TagLauncher"
+        return image
+    }
+
+    private func makeProStatusMenuItem() -> NSMenuItem {
+        let item = NSMenuItem(
+            title: "",
+            action: #selector(openProStatusFromStatusMenu(_:)),
+            keyEquivalent: ""
+        )
+        item.identifier = Self.proStatusMenuItemIdentifier
+        item.target = self
+        configureProStatusMenuItem(item)
+        return item
+    }
+
+    private func configureProStatusMenuItem(in menu: NSMenu) {
+        guard let item = menu.items.first(where: { $0.identifier == Self.proStatusMenuItemIdentifier }) else {
+            return
+        }
+        configureProStatusMenuItem(item)
+    }
+
+    private func configureProStatusMenuItem(_ item: NSMenuItem) {
+        let isUnlocked = ProEntitlementPolicy.accessState().isUnlocked
+        item.title = tr(isUnlocked ? "menu.proStatus.unlocked" : "menu.proStatus.free")
+        item.action = #selector(openProStatusFromStatusMenu(_:))
+        item.target = self
+        item.isEnabled = true
+        item.image = isUnlocked ? makeProStatusMenuIcon() : nil
+    }
+
+    private func makeProStatusMenuIcon() -> NSImage? {
+        guard let symbol = NSImage(systemSymbolName: "crown.fill", accessibilityDescription: "Pro") else {
+            return nil
+        }
+        let configuration = NSImage.SymbolConfiguration(pointSize: 13, weight: .semibold)
+        let configuredSymbol = symbol.withSymbolConfiguration(configuration) ?? symbol
+        let image = NSImage(size: NSSize(width: 16, height: 16))
+        image.lockFocus()
+        NSColor(red: 0.96, green: 0.63, blue: 0.10, alpha: 1.0).set()
+        let iconRect = NSRect(x: 1.5, y: 1.5, width: 13, height: 13)
+        configuredSymbol.draw(in: iconRect, from: .zero, operation: .sourceOver, fraction: 1.0)
+        NSColor(red: 0.96, green: 0.63, blue: 0.10, alpha: 1.0).set()
+        iconRect.fill(using: .sourceAtop)
+        image.unlockFocus()
+        image.isTemplate = false
+        image.accessibilityDescription = "Pro"
         return image
     }
 
@@ -1625,9 +1721,13 @@
             forName: .tagLauncherOpenPreferencesRequested,
             object: nil,
             queue: .main
-        ) { [weak self] _ in
-            self?.openPreferences()
+        ) { [weak self] notification in
+            self?.openPreferences(targetTab: Self.preferencesTabTarget(from: notification))
         }
+    }
+
+    private static func preferencesTabTarget(from notification: Notification) -> String? {
+        notification.userInfo?[SettingsTabTarget.userInfoKey] as? String
     }
 
     private func observeExternalActivationRequests() {
@@ -1649,6 +1749,14 @@
     }
 
     @objc private func openPreferences(_ sender: Any? = nil) {
+        openPreferences(targetTab: nil)
+    }
+
+    @objc private func openProStatusFromStatusMenu(_ sender: NSMenuItem) {
+        openPreferences(targetTab: SettingsTabTarget.about)
+    }
+
+    private func openPreferences(targetTab: String?) {
         dismissQuickSearchIfNeeded()
         TagDatabase.flushPendingCategorySchemeBackupBatch()
         if overlayAvoidsSpaceSwitch {
@@ -1664,6 +1772,7 @@
 
         if let settingsWindow {
             prepareSettingsWindow(settingsWindow)
+            requestPreferencesTabSelection(targetTab)
             return
         }
 
@@ -1674,10 +1783,19 @@
             defer: false
         )
         window.title = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
-        window.contentView = NSHostingView(rootView: PreferencesView())
+        window.contentView = NSHostingView(rootView: PreferencesView(initialTabRawValue: targetTab))
         window.isReleasedWhenClosed = false
         settingsWindow = window
         prepareSettingsWindow(window)
+    }
+
+    private func requestPreferencesTabSelection(_ targetTab: String?) {
+        guard let targetTab else { return }
+        NotificationCenter.default.post(
+            name: .tagLauncherPreferencesTabRequested,
+            object: nil,
+            userInfo: [SettingsTabTarget.userInfoKey: targetTab]
+        )
     }
 
     private func dismissQuickSearchIfNeeded() {
@@ -1751,7 +1869,13 @@
             super.mouseDown(with: event)
             return
         }
+        if routeUsageTipsMouseDownIfNeeded(event) {
+            return
+        }
         if hit == self {
+            if shouldSwallowUsageTipsBackdropClick(at: location) {
+                return
+            }
             if quickSearchSuppressesBackdropDismiss {
                 NotificationCenter.default.post(
                     name: .tagLauncherQuickSearchDismissRequested,
@@ -1786,6 +1910,32 @@
         super.mouseDown(with: event)
     }
 
+    private func routeUsageTipsMouseDownIfNeeded(_ event: NSEvent) -> Bool {
+        guard let appGridHost = findAppGridCollectionHost(in: self) else { return false }
+        return appGridHost.handleUsageTipsMouseDown(event)
+    }
+
+    private func findAppGridCollectionHost(in view: NSView) -> AppGridCollectionHostView? {
+        if let host = view as? AppGridCollectionHostView {
+            return host
+        }
+        for subview in view.subviews {
+            if let host = findAppGridCollectionHost(in: subview) {
+                return host
+            }
+        }
+        return nil
+    }
+
+    private func shouldSwallowUsageTipsBackdropClick(at location: NSPoint) -> Bool {
+        guard !UserDefaults.standard.bool(forKey: "hideUsageTips") else { return false }
+        let height = min(bounds.height, AppGridUsageTipsMetrics.reservedHeight)
+        guard height > 0 else { return false }
+        let y = isFlipped ? max(0, bounds.height - height) : 0
+        let region = NSRect(x: 0, y: y, width: bounds.width, height: height)
+        return region.contains(location)
+    }
+
     private func observeBackdropDismissSuppressionChanges() {
         modalInteractionObserver = NotificationCenter.default.addObserver(
             forName: .tagLauncherModalInteractionChanged,

--
Gitblit v1.9.3