From ec6e0da8224a54dd7273adc2a797f149fd9795c8 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 20 May 2026 12:51:53 +0800
Subject: [PATCH] chore: mark stable baseline as 7.6.0

---
 Apptag/DataLayer.swift |   75 ++++++++++++++++++++++++++++++++++---
 1 files changed, 69 insertions(+), 6 deletions(-)

diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index 518258d..59371d9 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -51,6 +51,10 @@
 // MARK: - App Scanner
 
 enum AppIndexer {
+    private static let scanCacheLock = NSLock()
+    private static var cachedScanApps: [AppInfo]? = nil
+    private static var cachedScanAt: Date? = nil
+    private static let scanCacheTTL: TimeInterval = 1800
 
     static let searchPaths: [URL] = [
         URL(fileURLWithPath: "/Applications"),
@@ -72,7 +76,42 @@
     }
 
     /// Scan all standard locations. Tags are annotated from TagDatabase by the caller.
-    static func scan() -> [AppInfo] {
+    static func scan(useCache: Bool = true) -> [AppInfo] {
+        if useCache, let cached = cachedScanIfFresh() {
+            return cached
+        }
+
+        let apps = performScan()
+        updateScanCache(apps)
+        return apps
+    }
+
+    static func invalidateScanCache() {
+        scanCacheLock.lock()
+        cachedScanApps = nil
+        cachedScanAt = nil
+        scanCacheLock.unlock()
+    }
+
+    private static func cachedScanIfFresh() -> [AppInfo]? {
+        scanCacheLock.lock()
+        defer { scanCacheLock.unlock() }
+
+        guard let cachedScanApps,
+              let cachedScanAt,
+              Date().timeIntervalSince(cachedScanAt) < scanCacheTTL
+        else { return nil }
+        return cachedScanApps
+    }
+
+    private static func updateScanCache(_ apps: [AppInfo]) {
+        scanCacheLock.lock()
+        cachedScanApps = apps
+        cachedScanAt = Date()
+        scanCacheLock.unlock()
+    }
+
+    private static func performScan() -> [AppInfo] {
         var seenResolvedPaths = Set<String>()
         var apps: [AppInfo] = []
 
@@ -220,12 +259,18 @@
         var seenAppIDsByGroup: [String: Set<URL>] = [:]
 
         for app in apps {
+            if app.isAppleApp {
+                appendGroupedApp(app, to: macCategory, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
+            }
+
             if app.tags.isEmpty {
-                let bucket = app.isAppleApp ? macCategory : defaultGroupName
-                appendGroupedApp(app, to: bucket, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
+                if !app.isAppleApp {
+                    appendGroupedApp(app, to: defaultGroupName, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
+                }
             } else {
                 for tag in uniqueOrdered(app.tags) {
                     let displayName = nameOverrides[tag] ?? tag
+                    guard displayName != macCategory else { continue }
                     appendGroupedApp(app, to: displayName, groups: &dict, seenAppIDsByGroup: &seenAppIDsByGroup)
                 }
             }
@@ -294,6 +339,7 @@
         var uncommonAppPaths: [String] = []  // special marker; does not affect normal groups
         var uncommonSources: [String: UncommonSource] = [:]  // current uncommon source: auto/manual
         var appOpenCounts: [String: Int] = [:]  // launches opened from TagLauncher
+        var appLastOpenedAt: [String: Date] = [:]  // successful launches opened from TagLauncher
         var knownAppPaths: [String] = []  // baseline set to detect newly installed apps
         var appNotes: [String: String] = [:]  // path → user note; retained even if marker is removed
         var disabledSystemCategoryIDs: [SmartCategoryID] = []  // system categories the user deleted
@@ -308,6 +354,7 @@
             case uncommonAppPaths
             case uncommonSources
             case appOpenCounts
+            case appLastOpenedAt
             case knownAppPaths
             case appNotes
             case disabledSystemCategoryIDs
@@ -326,6 +373,7 @@
             uncommonAppPaths = try container.decodeIfPresent([String].self, forKey: .uncommonAppPaths) ?? []
             uncommonSources = try container.decodeIfPresent([String: UncommonSource].self, forKey: .uncommonSources) ?? [:]
             appOpenCounts = try container.decodeIfPresent([String: Int].self, forKey: .appOpenCounts) ?? [:]
+            appLastOpenedAt = try container.decodeIfPresent([String: Date].self, forKey: .appLastOpenedAt) ?? [:]
             knownAppPaths = try container.decodeIfPresent([String].self, forKey: .knownAppPaths) ?? []
             appNotes = try container.decodeIfPresent([String: String].self, forKey: .appNotes) ?? [:]
             disabledSystemCategoryIDs = try container.decodeIfPresent(
@@ -1136,7 +1184,21 @@
         }
 
         let newPaths = scannedPaths.subtracting(knownPaths)
-        guard !newPaths.isEmpty else { return store }
+        let removedPaths = knownPaths.subtracting(scannedPaths)
+        if !removedPaths.isEmpty {
+            for path in removedPaths {
+                store.appOpenCounts.removeValue(forKey: path)
+                store.appLastOpenedAt.removeValue(forKey: path)
+            }
+            store.knownAppPaths = knownPaths.subtracting(removedPaths).sorted()
+        }
+
+        guard !newPaths.isEmpty else {
+            if !removedPaths.isEmpty {
+                TagDatabase.save(store)
+            }
+            return store
+        }
 
         var uncommonPaths = Set(store.uncommonAppPaths)
         let appsByPath = Dictionary(uniqueKeysWithValues: apps.map { ($0.path.path, $0) })
@@ -1156,7 +1218,7 @@
         let newApps = apps.filter { newPaths.contains($0.path.path) }
         _ = seedDefaultAppleAppNotes(for: newApps, in: &store)
         store.uncommonAppPaths = uncommonPaths.sorted()
-        store.knownAppPaths = knownPaths.union(newPaths).sorted()
+        store.knownAppPaths = Set(store.knownAppPaths).union(newPaths).sorted()
         TagDatabase.save(store)
         return store
     }
@@ -1208,6 +1270,7 @@
     static func recordLauncherOpen(for path: String) {
         var store = TagDatabase.load()
         store.appOpenCounts[path] = (store.appOpenCounts[path] ?? 0) + 1
+        store.appLastOpenedAt[path] = Date()
 
         if store.uncommonSources[path] == .auto,
            store.appOpenCounts[path, default: 0] >= TagDatabase.autoUncommonOpenThreshold {
@@ -1253,7 +1316,7 @@
         }
 
         var current = store.appTags[path] ?? []
-        if !copy, !sourceTag.isEmpty {
+        if !copy, !sourceTag.isEmpty, sourceTag != "Mac自带", sourceTag != tr("group.appleBuiltIn") {
             current.removeAll { $0 == sourceTag }
         }
         if !current.contains(targetTag) {

--
Gitblit v1.9.3