| | |
| | | // 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"), |
| | |
| | | } |
| | | |
| | | /// 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] = [] |
| | | |
| | |
| | | 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) |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | 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) { |