From 2a028d08370a86aeca124efc9a7b3e52ac32c367 Mon Sep 17 00:00:00 2001
From: Ariver <ar@MacBook-Air.local>
Date: Wed, 13 May 2026 23:53:08 +0800
Subject: [PATCH] Release TagLauncher 5.7.0

---
 Apptag/ApptagApp.swift |   93 +++++++++++++++++++++++++++++++++-------------
 1 files changed, 66 insertions(+), 27 deletions(-)

diff --git a/Apptag/ApptagApp.swift b/Apptag/ApptagApp.swift
index 76f0549..b385c01 100644
--- a/Apptag/ApptagApp.swift
+++ b/Apptag/ApptagApp.swift
@@ -12,7 +12,7 @@
         Settings {
             PreferencesView()
         }
-        .defaultSize(width: 880, height: 420)
+        .defaultSize(width: 880, height: 460)
     }
 }
 
@@ -24,7 +24,7 @@
 }
 
 final class AppDelegate: NSObject, NSApplicationDelegate {
-    private var statusItem: NSStatusItem!
+    private var statusItem: NSStatusItem?
     private var overlayWindow: NSWindow?
     private var settingsWindow: NSWindow?    // Track Settings window to keep it above overlay
     private var hotkeyRef: EventHotKeyRef?
@@ -150,17 +150,19 @@
     // MARK: - Menu Bar
 
     private func setupMenuBar() {
-        // Remove old status item if re-creating (language switch, etc.)
-        if let existing = statusItem {
-            NSStatusBar.system.removeStatusItem(existing)
+        if statusItem == nil {
+            statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
         }
-        statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
+        guard let statusItem else { return }
 
         if let button = statusItem.button {
-            button.image = NSImage(
+            let image = NSImage(
                 systemSymbolName: "tag.fill",
                 accessibilityDescription: "TagLauncher"
             )
+            image?.isTemplate = true
+            button.image = image
+            button.imagePosition = .imageOnly
             button.toolTip = "TagLauncher — Tag-based app launcher"
             button.action = #selector(toggleOverlay)
             button.target = self
@@ -365,8 +367,8 @@
                   !self.isInEditMode
             else { return }
 
-            // Settings/Preferences window → float it above overlay for real-time preview
-            if NSApp.windows.contains(keyWindow) {
+            // Settings/Preferences window -> float it above overlay for real-time preview.
+            if self.isSettingsWindowCandidate(keyWindow) {
                 self.prepareSettingsWindow(keyWindow)
                 return
             }
@@ -377,10 +379,11 @@
 
     /// Settings must always appear centered over the current overlay view and float above it.
     private func prepareSettingsWindow(_ window: NSWindow) {
-        let settingsSize = NSSize(width: 880, height: max(window.frame.height, 420))
-        window.minSize = NSSize(width: 880, height: 420)
-        window.maxSize = NSSize(width: 880, height: CGFloat.greatestFiniteMagnitude)
-        if abs(window.frame.width - settingsSize.width) > 0.5 || window.frame.height < settingsSize.height {
+        window.identifier = NSUserInterfaceItemIdentifier("TagLauncherPreferencesWindow")
+        let settingsSize = NSSize(width: 880, height: 460)
+        window.minSize = settingsSize
+        window.maxSize = settingsSize
+        if abs(window.frame.width - settingsSize.width) > 0.5 || abs(window.frame.height - settingsSize.height) > 0.5 {
             window.setFrame(
                 NSRect(origin: window.frame.origin, size: settingsSize),
                 display: false
@@ -399,6 +402,17 @@
         window.makeKeyAndOrderFront(nil)
         window.orderFrontRegardless()
         settingsWindow = window
+    }
+
+    private func isSettingsWindowCandidate(_ window: NSWindow) -> Bool {
+        if window == settingsWindow { return true }
+        if window.identifier?.rawValue == "TagLauncherPreferencesWindow" { return true }
+        guard NSApp.windows.contains(window),
+              window != overlayWindow,
+              window.isVisible,
+              !(window is NSPanel)
+        else { return false }
+        return true
     }
 
     private func center(_ window: NSWindow, over rect: NSRect) {
@@ -453,7 +467,7 @@
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.03) { [weak self] in
             guard let self else { return }
             let candidates = NSApp.windows.filter { window in
-                window != self.overlayWindow && window.isVisible
+                self.isSettingsWindowCandidate(window)
             }
             candidates.forEach { self.prepareSettingsWindow($0) }
             if candidates.isEmpty && retries > 0 {
@@ -559,7 +573,6 @@
     @AppStorage("showDockIcon") private var showDockIcon = false
     @AppStorage("launchAtLogin") private var launchAtLogin = true
     @State private var selectedLanguage = L10n.currentCode
-    @State private var languageRefreshID = UUID()
     @State private var isRefreshingLanguage = false
     @State private var allApps: [AppInfo] = []
     @State private var tagColors: [String: Int] = [:]
@@ -624,25 +637,53 @@
     private var buildVersion: String {
         Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "?"
     }
+    private var languageColumns: [GridItem] {
+        [
+            GridItem(.flexible(minimum: 220), spacing: 18, alignment: .top),
+            GridItem(.flexible(minimum: 220), spacing: 18, alignment: .top),
+        ]
+    }
 
     var body: some View {
         ZStack {
             TabView {
                 // Tab 1: Language
-                Form {
-                    Section {
-                        Picker(tr("settings.languagePicker"), selection: $selectedLanguage) {
+                VStack(alignment: .leading, spacing: 10) {
+                    Text(tr("settings.language"))
+                        .font(.headline)
+                    Text(tr("settings.languageDesc"))
+                        .font(.caption)
+                        .foregroundStyle(.secondary)
+
+                    ScrollView {
+                        LazyVGrid(columns: languageColumns, alignment: .leading, spacing: 6) {
                             ForEach(L10n.supported, id: \.code) { language in
-                                Text(language.name).tag(language.code)
+                                Button {
+                                    selectedLanguage = language.code
+                                } label: {
+                                    HStack(spacing: 8) {
+                                        Image(systemName: selectedLanguage == language.code ? "checkmark" : "circle")
+                                            .font(.system(size: 12, weight: .semibold))
+                                            .foregroundStyle(selectedLanguage == language.code ? Color.accentColor : Color.secondary.opacity(0.28))
+                                            .frame(width: 16)
+                                        Text(language.name)
+                                            .foregroundStyle(.primary)
+                                            .lineLimit(1)
+                                            .truncationMode(.tail)
+                                        Spacer(minLength: 0)
+                                    }
+                                    .padding(.horizontal, 8)
+                                    .padding(.vertical, 5)
+                                    .contentShape(Rectangle())
+                                }
+                                .buttonStyle(.plain)
+                                .accessibilityLabel("\(tr("settings.languagePicker")) \(language.name)")
                             }
                         }
-                        .pickerStyle(.radioGroup)
-                    } header: {
-                        Text(tr("settings.language"))
-                    } footer: {
-                        Text(tr("settings.languageDesc"))
+                        .padding(.vertical, 4)
                     }
                 }
+                .frame(maxWidth: settingsContentWidth, alignment: .leading)
                 .tabItem { Label(tr("settings.language"), systemImage: "globe") }
                 .padding()
 
@@ -812,7 +853,6 @@
             .tabItem { Label(tr("settings.about"), systemImage: "info.circle") }
             .padding()
             }
-            .id(languageRefreshID)
             .onChange(of: selectedLanguage) { _, code in
                 L10n.switchTo(code)
             }
@@ -835,12 +875,11 @@
                 .transition(.opacity)
             }
         }
-        .frame(minWidth: settingsWindowWidth, maxWidth: settingsWindowWidth, minHeight: 420)
+        .frame(width: settingsWindowWidth, height: 460)
     }
 
     private func showLanguageRefresh() {
         isRefreshingLanguage = true
-        languageRefreshID = UUID()
         DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
             withAnimation(.easeOut(duration: 0.18)) {
                 isRefreshingLanguage = false

--
Gitblit v1.9.3