@@ -200,6 +200,8 @@ struct ContentView: View {
200200
201201 private let localAppsURL = URL ( fileURLWithPath: " /Applications " )
202202 @State private var externalDriveURL : URL ?
203+ @State private var customLocalScanPaths : [ String ] = UserDefaults . standard. stringArray ( forKey: " customLocalScanPaths " ) ?? [ ]
204+ @State private var customLocalMonitors : [ FolderMonitor ] = [ ]
203205
204206 // 多选支持
205207 @State private var selectedLocalApps : Set < String > = [ ]
@@ -401,10 +403,43 @@ struct ContentView: View {
401403 // --- 左侧:本地应用 ---
402404 VStack ( spacing: 0 ) {
403405 // Header Area (Restored to original simple style)
404- HeaderView ( title: " Mac 本地应用 " . localized, subtitle: " /Applications " , icon: " macmini " ) {
405- scanLocalApps ( )
406+ HeaderView (
407+ title: " Mac 本地应用 " . localized,
408+ subtitle: localAppsSubtitle,
409+ icon: " macmini " ,
410+ actionButtonText: " + " ,
411+ onAction: addCustomLocalScanPath,
412+ onRefresh: { scanLocalApps ( ) }
413+ )
414+
415+ // 自定义扫描目录标签
416+ if !customLocalScanPaths. isEmpty {
417+ ScrollView ( . horizontal, showsIndicators: false ) {
418+ HStack ( spacing: 6 ) {
419+ ForEach ( customLocalScanPaths, id: \. self) { path in
420+ HStack ( spacing: 4 ) {
421+ Text ( ( path as NSString ) . lastPathComponent)
422+ . font ( . caption)
423+ . lineLimit ( 1 )
424+ Button ( action: { removeCustomLocalScanPath ( path) } ) {
425+ Image ( systemName: " xmark.circle.fill " )
426+ . font ( . caption2)
427+ . foregroundColor ( . secondary)
428+ }
429+ . buttonStyle ( . plain)
430+ }
431+ . padding ( . horizontal, 8 )
432+ . padding ( . vertical, 4 )
433+ . background ( Color . accentColor. opacity ( 0.1 ) )
434+ . cornerRadius ( 6 )
435+ }
436+ }
437+ . padding ( . horizontal, 20 )
438+ . padding ( . vertical, 6 )
439+ }
440+ . background ( . ultraThinMaterial)
406441 }
407-
442+
408443 ZStack {
409444 Color ( nsColor: . controlBackgroundColor) . ignoresSafeArea ( )
410445
@@ -1095,31 +1130,59 @@ struct ContentView: View {
10951130 Task . detached ( priority: . userInitiated) {
10961131 // Gather data needed for scanning
10971132 let runningAppURLs = await MainActor . run { self . getRunningAppURLs ( ) }
1098- let scanDir = self . localAppsURL
10991133 let externalAppsDir = await MainActor . run { self . externalDriveURL }
1100-
1134+ let customPaths = await MainActor . run { self . customLocalScanPaths }
1135+
11011136 // Use Actor
11021137 let scanner = AppScanner ( )
1103- let newApps = await scanner. scanLocalApps (
1104- at: scanDir ,
1138+ var allApps = await scanner. scanLocalApps (
1139+ at: self . localAppsURL ,
11051140 runningAppURLs: runningAppURLs,
11061141 externalAppsDir: externalAppsDir
11071142 )
1108-
1143+
1144+ // 扫描自定义目录
1145+ for path in customPaths {
1146+ let customApps = await scanner. scanLocalApps (
1147+ at: URL ( fileURLWithPath: path) ,
1148+ runningAppURLs: runningAppURLs,
1149+ externalAppsDir: externalAppsDir
1150+ )
1151+ let existingPaths = Set ( allApps. map { $0. path. path } )
1152+ for app in customApps where !existingPaths. contains ( app. path. path) {
1153+ allApps. append ( app)
1154+ }
1155+ }
1156+
1157+ let finalApps = allApps
1158+
1159+ // 检测外置 app 版本变化,刷新本地 Stub Portal
1160+ if let externalDir = externalAppsDir {
1161+ let externalApps = await scanner. scanExternalApps ( at: externalDir, localAppsDir: URL ( fileURLWithPath: " /Applications " ) )
1162+ let service = AppMigrationService ( )
1163+ for localApp in finalApps where localApp. status == AppStatus . linked {
1164+ if let externalApp = externalApps. first ( where: { $0. name == localApp. name } ) ,
1165+ localApp. version != externalApp. version {
1166+ service. refreshStubPortal ( at: localApp. path, from: externalApp. path)
1167+ }
1168+ }
1169+ }
1170+
11091171 await MainActor . run {
1110- self . localApps = newApps
1172+ self . localApps = finalApps
11111173 }
11121174 AppLogger . shared. logContext (
11131175 " 本地应用扫描完成 " ,
11141176 details: [
11151177 ( " scan_id " , scanID) ,
1116- ( " count " , String ( newApps. count) ) ,
1117- ( " status_summary " , self . summarizeStatuses ( for: newApps) )
1178+ ( " count " , String ( finalApps. count) ) ,
1179+ ( " custom_dirs " , String ( customPaths. count) ) ,
1180+ ( " status_summary " , self . summarizeStatuses ( for: finalApps) )
11181181 ]
11191182 )
11201183
11211184 if calculateSizes {
1122- await self . calculateSizesProgressive ( for: newApps , isLocal: true , scanner: scanner)
1185+ await self . calculateSizesProgressive ( for: finalApps , isLocal: true , scanner: scanner)
11231186 }
11241187 }
11251188 }
@@ -1173,14 +1236,33 @@ struct ContentView: View {
11731236 func calculateSizesProgressive( for apps: [ AppItem ] , isLocal: Bool , scanner: AppScanner ) async {
11741237 // 并行计算所有大小,再一次性批量更新 UI,避免列表反复重排
11751238 let results = await withTaskGroup ( of: ( String, Int64) . self) { group in
1176- for app in apps {
1239+ var dict : [ String : Int64 ] = [ : ]
1240+ var iterator = apps. makeIterator ( )
1241+ var active = 0
1242+ let maxConcurrency = 4
1243+
1244+ // 启动初始批次
1245+ for _ in 0 ..< min ( maxConcurrency, apps. count) {
1246+ guard let app = iterator. next ( ) else { break }
11771247 group. addTask {
11781248 let size = await scanner. calculateDisplayedSize ( for: app, isLocalEntry: isLocal)
11791249 return ( app. id, size)
11801250 }
1251+ active += 1
1252+ }
1253+
1254+ // 每完成一个再启动一个
1255+ for await (id, size) in group {
1256+ dict [ id] = size
1257+ active -= 1
1258+ if let app = iterator. next ( ) {
1259+ group. addTask {
1260+ let size = await scanner. calculateDisplayedSize ( for: app, isLocalEntry: isLocal)
1261+ return ( app. id, size)
1262+ }
1263+ active += 1
1264+ }
11811265 }
1182- var dict : [ String : Int64 ] = [ : ]
1183- for await (id, size) in group { dict [ id] = size }
11841266 return dict
11851267 }
11861268
@@ -1255,7 +1337,40 @@ struct ContentView: View {
12551337 AppLogger . shared. log ( " 用户取消选择外部路径 " , level: " TRACE " )
12561338 }
12571339 }
1258-
1340+
1341+ // MARK: - 自定义本地扫描目录
1342+
1343+ private var localAppsSubtitle : String {
1344+ let base = " /Applications "
1345+ if customLocalScanPaths. isEmpty {
1346+ return base
1347+ }
1348+ return " \( base) + \( customLocalScanPaths. count) \( " 个目录 " . localized) "
1349+ }
1350+
1351+ func addCustomLocalScanPath( ) {
1352+ let panel = NSOpenPanel ( )
1353+ panel. prompt = " 选择文件夹 " . localized
1354+ panel. allowsMultipleSelection = false
1355+ panel. canChooseDirectories = true
1356+ panel. canChooseFiles = false
1357+ panel. message = " 选择要额外扫描的应用目录 " . localized
1358+ guard panel. runModal ( ) == . OK, let url = panel. url else { return }
1359+ let path = url. path
1360+ guard !customLocalScanPaths. contains ( path) else { return }
1361+ customLocalScanPaths. append ( path)
1362+ UserDefaults . standard. set ( customLocalScanPaths, forKey: " customLocalScanPaths " )
1363+ startMonitoringLocal ( )
1364+ scanLocalApps ( )
1365+ }
1366+
1367+ func removeCustomLocalScanPath( _ path: String ) {
1368+ customLocalScanPaths. removeAll { $0 == path }
1369+ UserDefaults . standard. set ( customLocalScanPaths, forKey: " customLocalScanPaths " )
1370+ startMonitoringLocal ( )
1371+ scanLocalApps ( )
1372+ }
1373+
12591374 func showError( title: String , message: String ) {
12601375 AppLogger . shared. logContext (
12611376 " 向用户展示错误 " ,
@@ -2100,13 +2215,26 @@ struct ContentView: View {
21002215
21012216 func startMonitoringLocal( ) {
21022217 localMonitor? . stopMonitoring ( )
2218+ customLocalMonitors. forEach { $0. stopMonitoring ( ) }
2219+ customLocalMonitors. removeAll ( )
2220+
21032221 AppLogger . shared. logContext ( " 启动本地目录监控 " , details: [ ( " path " , localAppsURL. path) ] )
21042222
21052223 let monitor = FolderMonitor ( url: localAppsURL)
21062224 monitor. startMonitoring { [ self ] in
21072225 scheduleMonitorRescan ( local: true )
21082226 }
21092227 self . localMonitor = monitor
2228+
2229+ // 监控自定义目录
2230+ for path in customLocalScanPaths {
2231+ let url = URL ( fileURLWithPath: path)
2232+ let customMonitor = FolderMonitor ( url: url)
2233+ customMonitor. startMonitoring { [ self ] in
2234+ scheduleMonitorRescan ( local: true )
2235+ }
2236+ customLocalMonitors. append ( customMonitor)
2237+ }
21102238 }
21112239
21122240 func startMonitoringExternal( url: URL ) {
@@ -2136,6 +2264,7 @@ struct ContentView: View {
21362264 let runningAppURLs = await MainActor . run { self . getRunningAppURLs ( ) }
21372265 let localDir = self . localAppsURL
21382266 let externalLocalDir = URL ( fileURLWithPath: " /Applications " )
2267+ let customPaths = await MainActor . run { self . customLocalScanPaths }
21392268
21402269 // 保留旧的大小信息
21412270 let oldLocalSizes = await MainActor . run {
@@ -2152,18 +2281,31 @@ struct ContentView: View {
21522281 }
21532282
21542283 // 并行扫描
2155- async let localResult = scanner. scanLocalApps (
2284+ var newLocalApps = await scanner. scanLocalApps (
21562285 at: localDir,
21572286 runningAppURLs: runningAppURLs,
21582287 externalAppsDir: externalDir
21592288 )
2289+
2290+ // 扫描自定义目录
2291+ for path in customPaths {
2292+ let customApps = await scanner. scanLocalApps (
2293+ at: URL ( fileURLWithPath: path) ,
2294+ runningAppURLs: runningAppURLs,
2295+ externalAppsDir: externalDir
2296+ )
2297+ let existingPaths = Set ( newLocalApps. map { $0. path. path } )
2298+ for app in customApps where !existingPaths. contains ( app. path. path) {
2299+ newLocalApps. append ( app)
2300+ }
2301+ }
2302+
21602303 let externalResult : [ AppItem ]
21612304 if let externalDir {
21622305 externalResult = await scanner. scanExternalApps ( at: externalDir, localAppsDir: externalLocalDir)
21632306 } else {
21642307 externalResult = [ ]
21652308 }
2166- var newLocalApps = await localResult
21672309
21682310 // 恢复旧的大小信息,只对变化的 app 标记需要重算
21692311 for i in newLocalApps. indices {
@@ -2180,6 +2322,15 @@ struct ContentView: View {
21802322 }
21812323 }
21822324
2325+ // 检测外置 app 版本变化,刷新本地 Stub Portal
2326+ let service = AppMigrationService ( )
2327+ for localApp in newLocalApps where localApp. status == AppStatus . linked {
2328+ if let externalApp = newExternalApps. first ( where: { $0. name == localApp. name } ) ,
2329+ localApp. version != externalApp. version {
2330+ service. refreshStubPortal ( at: localApp. path, from: externalApp. path)
2331+ }
2332+ }
2333+
21832334 let finalLocalApps = newLocalApps
21842335 let finalExternalApps = newExternalApps
21852336 await MainActor . run {
0 commit comments