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 | 312 +++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 287 insertions(+), 25 deletions(-)
diff --git a/Apptag/DataLayer.swift b/Apptag/DataLayer.swift
index d1a09a8..17a0605 100644
--- a/Apptag/DataLayer.swift
+++ b/Apptag/DataLayer.swift
@@ -11,9 +11,53 @@
let tags: [String]
let bundleIdentifier: String?
let localizedNames: [String]
+ let localizedNamesByLanguage: [String: String]
let icon: NSImage // Pre-loaded during background scan
var isUncommon: Bool = false
var note: String? = nil
+
+ var displayName: String {
+ localizedDisplayName(for: L10n.currentCode)
+ }
+
+ func localizedDisplayName(for languageCode: String) -> String {
+ let candidates = AppInfo.displayLanguageFallbacks(for: languageCode)
+ for code in candidates {
+ if let localizedName = localizedNamesByLanguage[code],
+ !localizedName.isEmpty {
+ return localizedName
+ }
+ }
+ if let localizedName = localizedNames.first, !localizedName.isEmpty {
+ return localizedName
+ }
+ return name
+ }
+
+ private static func displayLanguageFallbacks(for languageCode: String) -> [String] {
+ var candidates = [languageCode]
+ switch languageCode {
+ case "zh-Hans":
+ candidates.append("zh-Hant")
+ case "zh-Hant":
+ candidates.append("zh-Hans")
+ case "nb":
+ candidates.append("no")
+ case "nn":
+ candidates.append("no")
+ case "no":
+ candidates.append(contentsOf: ["nb", "nn"])
+ default:
+ break
+ }
+ candidates.append("en")
+ return uniqueLanguageCodes(candidates)
+ }
+
+ fileprivate static func uniqueLanguageCodes(_ codes: [String]) -> [String] {
+ var seen = Set<String>()
+ return codes.filter { seen.insert($0).inserted }
+ }
/// True if this is an Apple pre-installed app.
var isAppleApp: Bool {
@@ -53,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
@@ -69,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/"
]
@@ -79,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
}
@@ -92,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] {
@@ -174,16 +262,30 @@
guard !isNestedInsideAppBundle(resolvedURL) else { return }
guard seenResolvedPaths.insert(resolvedURL.path).inserted else { return }
+ let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
+ let bundleId = bundle?.bundleIdentifier
+ guard !isTagLauncherBundle(bundleId) else { return }
+
let name = displayURL.deletingPathExtension().lastPathComponent
let icon = NSWorkspace.shared.icon(forFile: displayURL.path)
icon.size = NSSize(width: 96, height: 96)
-
- let bundle = Bundle(url: displayURL) ?? Bundle(url: resolvedURL)
- let bundleId = bundle?.bundleIdentifier
- let localizedNames = localizedAppNames(
- for: displayURL,
+ 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,
+ localizedNamesByLanguage: localizedNamesByLanguage
)
apps.append(AppInfo(
@@ -192,24 +294,155 @@
tags: [],
bundleIdentifier: bundleId,
localizedNames: localizedNames,
+ localizedNamesByLanguage: localizedNamesByLanguage,
icon: icon
))
}
- private static func localizedAppNames(
- for appURL: URL,
+ private static func isTagLauncherBundle(_ bundleIdentifier: String?) -> Bool {
+ bundleIdentifier?.caseInsensitiveCompare(AppIdentity.bundleIdentifier) == .orderedSame
+ }
+
+ private static func localizedAppNameMap(
bundle: Bundle?,
fallbackName: String
+ ) -> [String: String] {
+ guard let bundle else { return [:] }
+ let loctable = infoPlistLoctable(in: bundle)
+ var result: [String: String] = [:]
+
+ for language in L10n.supported {
+ guard let localizedName = localizedAppName(
+ for: language.code,
+ bundle: bundle,
+ loctable: loctable,
+ fallbackName: fallbackName
+ ) else { continue }
+ result[language.code] = localizedName
+ }
+
+ return result
+ }
+
+ private static func localizedAppNames(
+ for appURL: URL,
+ fallbackName: String,
+ localizedNamesByLanguage: [String: String]
) -> [String] {
- var values: [String?] = []
- values.append(bundle?.localizedInfoDictionary?["CFBundleDisplayName"] as? String)
- values.append(bundle?.localizedInfoDictionary?["CFBundleName"] as? String)
- values.append(bundle?.infoDictionary?["CFBundleDisplayName"] as? String)
- values.append(bundle?.infoDictionary?["CFBundleName"] as? 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: ""))
return uniqueLocalizedNames(values, excluding: fallbackName)
+ }
+
+ private static func localizedAppName(
+ for languageCode: String,
+ bundle: Bundle,
+ loctable: [String: [String: Any]],
+ fallbackName: String
+ ) -> String? {
+ for localization in localizationCandidates(for: languageCode) {
+ if let table = loctable[localization],
+ let value = firstValidLocalizedName(
+ [table["CFBundleDisplayName"], table["CFBundleName"]],
+ excluding: fallbackName
+ ) {
+ return value
+ }
+
+ if let strings = infoPlistStrings(in: bundle, localization: localization),
+ let value = firstValidLocalizedName(
+ [strings["CFBundleDisplayName"], strings["CFBundleName"]],
+ excluding: fallbackName
+ ) {
+ return value
+ }
+ }
+
+ if languageCode == "en",
+ let value = firstValidLocalizedName(
+ [
+ bundle.infoDictionary?["CFBundleDisplayName"],
+ bundle.infoDictionary?["CFBundleName"]
+ ],
+ excluding: fallbackName
+ ) {
+ return value
+ }
+
+ return nil
+ }
+
+ private static func infoPlistLoctable(in bundle: Bundle) -> [String: [String: Any]] {
+ guard let url = bundle.url(forResource: "InfoPlist", withExtension: "loctable"),
+ let rawTable = NSDictionary(contentsOf: url) as? [String: Any]
+ else { return [:] }
+
+ var result: [String: [String: Any]] = [:]
+ for (key, value) in rawTable {
+ if let localizedTable = value as? [String: Any] {
+ result[key] = localizedTable
+ }
+ }
+ return result
+ }
+
+ private static func infoPlistStrings(
+ in bundle: Bundle,
+ localization: String
+ ) -> [String: Any]? {
+ guard let url = bundle.url(
+ forResource: "InfoPlist",
+ withExtension: "strings",
+ subdirectory: nil,
+ localization: localization
+ ) else { return nil }
+ return NSDictionary(contentsOf: url) as? [String: Any]
+ }
+
+ private static func localizationCandidates(for languageCode: String) -> [String] {
+ var candidates = [languageCode, languageCode.replacingOccurrences(of: "-", with: "_")]
+ switch languageCode {
+ case "zh-Hans":
+ candidates.append(contentsOf: ["zh_CN", "zh"])
+ case "zh-Hant":
+ candidates.append(contentsOf: ["zh_TW", "zh_HK", "zh"])
+ case "pt-BR":
+ candidates.append(contentsOf: ["pt_BR", "pt"])
+ case "sr-Cyrl":
+ candidates.append(contentsOf: ["sr_Cyrl", "sr"])
+ case "ar-Najdi":
+ candidates.append(contentsOf: ["ar_Najdi", "ar"])
+ case "nb":
+ candidates.append(contentsOf: ["nb", "no"])
+ case "nn":
+ candidates.append(contentsOf: ["nn", "no"])
+ case "no":
+ candidates.append(contentsOf: ["no", "nb", "nn"])
+ default:
+ if let base = languageCode.split(separator: "-").first.map(String.init) {
+ candidates.append(base)
+ }
+ }
+ return AppInfo.uniqueLanguageCodes(candidates)
+ }
+
+ private static func firstValidLocalizedName(
+ _ values: [Any?],
+ excluding excludedValue: String
+ ) -> String? {
+ let normalizedExcluded = normalizedLocalizedName(excludedValue)
+ for value in values {
+ guard let value = value as? String else { continue }
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty,
+ normalizedLocalizedName(trimmed) != normalizedExcluded
+ else { continue }
+ return trimmed
+ }
+ return nil
}
private static func spotlightDisplayName(for appURL: URL) -> String? {
@@ -218,6 +451,15 @@
else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
+ 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
}
@@ -272,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))"
}
@@ -391,6 +633,7 @@
struct AppNoteMetadata: Codable, Equatable {
var origin: AppNoteOrigin
var catalog: SmartDefaultNoteProvenance? = nil
+ var apple: SmartDefaultNoteProvenance? = nil
var noteFingerprint: String
}
@@ -1081,6 +1324,7 @@
name: app.name, path: app.path, tags: appTags,
bundleIdentifier: app.bundleIdentifier,
localizedNames: app.localizedNames,
+ localizedNamesByLanguage: app.localizedNamesByLanguage,
icon: app.icon,
isUncommon: uncommonPaths.contains(app.path.path),
note: store.appNotes[app.path.path]
@@ -1247,7 +1491,7 @@
for path in newPaths {
if let app = appsByPath[path],
app.isAppleApp,
- AppleDefaultAppNotes.isFamiliarAppleApp(app) {
+ AppleDefaultAppCatalog.isFamiliarAppleApp(app) {
continue
}
uncommonPaths.insert(path)
@@ -1273,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
}
@@ -1297,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
@@ -1337,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