From 4f5d7c13209555a114761f5027299f3ba1b42217 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 08 Jun 2026 13:27:09 +0800
Subject: [PATCH] Release 7.8.22 Apple default app catalog

---
 Apptag/DataLayer.swift |  113 +++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 files changed, 99 insertions(+), 14 deletions(-)

diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index 9f9b9f0..17a0605 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -28,6 +28,9 @@
                 return localizedName
             }
         }
+        if let localizedName = localizedNames.first, !localizedName.isEmpty {
+            return localizedName
+        }
         return name
     }
 
@@ -94,14 +97,22 @@
 // MARK: - App Scanner
 
 enum AppIndexer {
+    private struct SearchPathSignature: Equatable {
+        let path: String
+        let exists: Bool
+        let contentModificationTime: TimeInterval?
+    }
+
     private static let scanCacheLock = NSLock()
     private static var cachedScanApps: [AppInfo]? = nil
     private static var cachedScanAt: Date? = nil
+    private static var cachedSearchPathSignature: [SearchPathSignature]? = nil
     private static let scanCacheTTL: TimeInterval = 1800
 
     static let searchPaths: [URL] = [
         URL(fileURLWithPath: "/Applications"),
         URL(fileURLWithPath: "/System/Applications"),
+        URL(fileURLWithPath: "/System/Library/CoreServices/Applications"),
         URL(fileURLWithPath: "/System/Cryptexes/App/System/Applications"),
         URL(fileURLWithPath: "/System/Volumes/Preboot/Cryptexes/App/System/Applications"),
         FileManager.default.homeDirectoryForCurrentUser
@@ -110,6 +121,7 @@
 
     private static let systemAppPathPrefixes = [
         "/System/Applications/",
+        "/System/Library/CoreServices/Applications/",
         "/System/Cryptexes/App/System/Applications/",
         "/System/Volumes/Preboot/Cryptexes/App/System/Applications/"
     ]
@@ -120,12 +132,13 @@
 
     /// Scan all standard locations. Tags are annotated from TagDatabase by the caller.
     static func scan(useCache: Bool = true) -> [AppInfo] {
-        if useCache, let cached = cachedScanIfFresh() {
+        let searchPathSignature = currentSearchPathSignature()
+        if useCache, let cached = cachedScanIfFresh(matching: searchPathSignature) {
             return cached
         }
 
         let apps = performScan()
-        updateScanCache(apps)
+        updateScanCache(apps, searchPathSignature: searchPathSignature)
         return apps
     }
 
@@ -133,25 +146,59 @@
         scanCacheLock.lock()
         cachedScanApps = nil
         cachedScanAt = nil
+        cachedSearchPathSignature = nil
         scanCacheLock.unlock()
     }
 
-    private static func cachedScanIfFresh() -> [AppInfo]? {
+    static func shouldRefreshForSearchPathChanges() -> Bool {
+        scanCacheLock.lock()
+        let hasCachedApps = cachedScanApps != nil
+        let cachedSignature = cachedSearchPathSignature
+        scanCacheLock.unlock()
+
+        guard hasCachedApps else { return true }
+        return cachedSignature != currentSearchPathSignature()
+    }
+
+    private static func cachedScanIfFresh(matching searchPathSignature: [SearchPathSignature]) -> [AppInfo]? {
         scanCacheLock.lock()
         defer { scanCacheLock.unlock() }
 
         guard let cachedScanApps,
               let cachedScanAt,
+              cachedSearchPathSignature == searchPathSignature,
               Date().timeIntervalSince(cachedScanAt) < scanCacheTTL
         else { return nil }
         return cachedScanApps
     }
 
-    private static func updateScanCache(_ apps: [AppInfo]) {
+    private static func updateScanCache(_ apps: [AppInfo], searchPathSignature: [SearchPathSignature]) {
         scanCacheLock.lock()
         cachedScanApps = apps
         cachedScanAt = Date()
+        cachedSearchPathSignature = searchPathSignature
         scanCacheLock.unlock()
+    }
+
+    private static func currentSearchPathSignature() -> [SearchPathSignature] {
+        searchPaths.map { url in
+            var isDirectory: ObjCBool = false
+            let exists = FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory)
+            guard exists, isDirectory.boolValue else {
+                return SearchPathSignature(
+                    path: url.path,
+                    exists: false,
+                    contentModificationTime: nil
+                )
+            }
+
+            let values = try? url.resourceValues(forKeys: [.contentModificationDateKey])
+            return SearchPathSignature(
+                path: url.path,
+                exists: true,
+                contentModificationTime: values?.contentModificationDate?.timeIntervalSinceReferenceDate
+            )
+        }
     }
 
     private static func performScan() -> [AppInfo] {
@@ -222,10 +269,19 @@
         let name = displayURL.deletingPathExtension().lastPathComponent
         let icon = NSWorkspace.shared.icon(forFile: displayURL.path)
         icon.size = NSSize(width: 96, height: 96)
-        let localizedNamesByLanguage = localizedAppNameMap(
+        var localizedNamesByLanguage = localizedAppNameMap(
             bundle: bundle,
             fallbackName: name
         )
+        if let bundleId {
+            for (languageCode, displayName) in AppleDefaultAppCatalog.localizedNamesByLanguage(
+                forBundleIdentifier: bundleId
+            ) {
+                if localizedNamesByLanguage[languageCode]?.isEmpty != false {
+                    localizedNamesByLanguage[languageCode] = displayName
+                }
+            }
+        }
         let localizedNames = localizedAppNames(
             for: displayURL,
             fallbackName: name,
@@ -274,6 +330,7 @@
         localizedNamesByLanguage: [String: String]
     ) -> [String] {
         var values = L10n.supported.map { localizedNamesByLanguage[$0.code] }
+        values.append(resourceLocalizedName(for: appURL))
         values.append(spotlightDisplayName(for: appURL))
         values.append(FileManager.default.displayName(atPath: appURL.path).replacingOccurrences(of: ".app", with: ""))
 
@@ -397,6 +454,15 @@
         return trimmed.isEmpty ? nil : trimmed
     }
 
+    private static func resourceLocalizedName(for appURL: URL) -> String? {
+        guard let value = try? appURL.resourceValues(forKeys: [.localizedNameKey]).localizedName else {
+            return nil
+        }
+
+        let trimmed = value.replacingOccurrences(of: ".app", with: "").trimmingCharacters(in: .whitespacesAndNewlines)
+        return trimmed.isEmpty ? nil : trimmed
+    }
+
     private static func uniqueLocalizedNames(_ values: [String?], excluding excludedValue: String) -> [String] {
         let normalizedExcluded = normalizedLocalizedName(excludedValue)
         var seen = Set<String>()
@@ -448,7 +514,7 @@
     private static func deduplicationIdentity(for app: AppInfo) -> String {
         if let bundleIdentifier = app.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
            !bundleIdentifier.isEmpty {
-            return "bundle:\(bundleIdentifier.lowercased())"
+            return "bundle:\(bundleIdentifier.lowercased())|name:\(normalizedAppName(app.name))"
         }
         return "name:\(normalizedAppName(app.name))"
     }
@@ -567,6 +633,7 @@
     struct AppNoteMetadata: Codable, Equatable {
         var origin: AppNoteOrigin
         var catalog: SmartDefaultNoteProvenance? = nil
+        var apple: SmartDefaultNoteProvenance? = nil
         var noteFingerprint: String
     }
 
@@ -1424,7 +1491,7 @@
         for path in newPaths {
             if let app = appsByPath[path],
                app.isAppleApp,
-               AppleDefaultAppNotes.isFamiliarAppleApp(app) {
+               AppleDefaultAppCatalog.isFamiliarAppleApp(app) {
                 continue
             }
             uncommonPaths.insert(path)
@@ -1450,16 +1517,31 @@
 
         for app in apps where app.isAppleApp {
             let path = app.path.path
-            if store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false {
+
+            let currentNote = store.appNotes[path]?.trimmingCharacters(in: .whitespacesAndNewlines)
+            let existingMetadata = store.appNoteMetadata[path]
+            if existingMetadata?.origin == .manual {
                 continue
             }
 
-            guard let note = AppleDefaultAppNotes.note(for: app) else { continue }
-            let limited = String(note.prefix(TagDatabase.maxAppNoteLength))
-            store.appNotes[path] = limited
+            if let currentNote, !currentNote.isEmpty {
+                let currentFingerprint = TagDatabase.noteFingerprint(currentNote)
+                let currentMatchesDefault = existingMetadata?.origin == .appleDefault
+                    && currentFingerprint == existingMetadata?.noteFingerprint
+                guard currentMatchesDefault else {
+                    continue
+                }
+            }
+
+            guard let defaultNote = AppleDefaultAppCatalog.defaultNote(for: app),
+                  defaultNote.note != currentNote
+            else { continue }
+
+            store.appNotes[path] = defaultNote.note
             store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
                 origin: .appleDefault,
-                noteFingerprint: TagDatabase.noteFingerprint(limited)
+                apple: defaultNote.provenance,
+                noteFingerprint: defaultNote.provenance.noteFingerprint
             )
             changed = true
         }
@@ -1474,7 +1556,7 @@
         var uncommonPaths = Set(store.uncommonAppPaths)
         var changed = false
 
-        for app in apps where app.isAppleApp && !AppleDefaultAppNotes.isFamiliarAppleApp(app) {
+        for app in apps where app.isAppleApp && !AppleDefaultAppCatalog.isFamiliarAppleApp(app) {
             let path = app.path.path
             if uncommonPaths.insert(path).inserted {
                 changed = true
@@ -1514,7 +1596,10 @@
         let limited = String(trimmed.prefix(TagDatabase.maxAppNoteLength))
         if limited.isEmpty {
             store.appNotes.removeValue(forKey: path)
-            store.appNoteMetadata.removeValue(forKey: path)
+            store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(
+                origin: .manual,
+                noteFingerprint: TagDatabase.noteFingerprint("")
+            )
         } else {
             store.appNotes[path] = limited
             store.appNoteMetadata[path] = TagDatabase.AppNoteMetadata(

--
Gitblit v1.9.3