@@ -66,7 +66,6 @@ struct PluginsView: View {
6666 @EnvironmentObject var appState : AppState
6767 @EnvironmentObject var locations : Locations
6868 @Environment ( \. colorScheme) var colorScheme
69- @State private var plugins : [ String : [ PluginInfo ] ] = [ : ]
7069 @State private var allPlugins : [ PluginInfo ] = [ ]
7170 @State private var isLoading : Bool = false
7271 @State private var lastRefreshDate : Date ?
@@ -343,7 +342,6 @@ struct PluginsView: View {
343342 private func refreshPluginsAsync( ) async {
344343 await MainActor . run {
345344 isLoading = true
346- plugins = [ : ]
347345 allPlugins = [ ]
348346 selectedPlugins = [ ]
349347 selectedPluginPaths = [ ]
@@ -363,30 +361,24 @@ struct PluginsView: View {
363361 let fileManager = FileManager . default
364362 let pluginCategories = locations. plugins. subcategories
365363
366- // Process categories concurrently but update UI incrementally
367- await withTaskGroup ( of: ( String , [ PluginInfo ] ) . self) { group in
364+ // Process categories concurrently with incremental UI updates
365+ await withTaskGroup ( of: Void . self) { group in
368366
369367 // Add tasks for each category
370368 for (category, paths) in pluginCategories {
371369 group. addTask {
372- return await self . processCategory ( category: category, paths: paths, fileManager: fileManager)
370+ await self . processCategory ( category: category, paths: paths, fileManager: fileManager)
373371 }
374372 }
375373
376- // Collect results and update UI incrementally
377- for await (category, categoryPlugins) in group {
378- if !categoryPlugins. isEmpty {
379- await MainActor . run {
380- self . plugins [ category] = categoryPlugins
381- self . allPlugins. append ( contentsOf: categoryPlugins)
382- }
383- }
384- }
374+ // Wait for all categories to complete (no results to collect - processCategory updates UI directly)
375+ await group. waitForAll ( )
385376 }
386377 }
387378
388- private func processCategory( category: String , paths: [ String ] , fileManager: FileManager ) async -> ( String , [ PluginInfo ] ) {
389- var categoryPlugins : [ PluginInfo ] = [ ]
379+ private func processCategory( category: String , paths: [ String ] , fileManager: FileManager ) async {
380+ var batchBuffer : [ PluginInfo ] = [ ]
381+ let batchSize = 20 // Update UI every 20 plugins for smooth incremental loading
390382
391383 for path in paths {
392384 // Check if path exists before trying to read it
@@ -415,13 +407,8 @@ struct PluginsView: View {
415407 if !name. hasPrefix ( " . " ) && !name. hasPrefix ( " ~ " ) {
416408 // Check if file matches the expected types for this category
417409 if shouldIncludeFile ( name: name, isDirectory: isDirectory, category: category) {
418- // Extract bundle info for Audio .driver bundles
419- let bundleId = await extractBundleId ( from: itemURL. path, category: category, isDirectory: isDirectory)
420-
421- // Skip Apple system plugins for Audio category
422- if category == " Audio " , let bundleId = bundleId, bundleId. contains ( " com.apple " ) {
423- continue
424- }
410+ // Bundle ID will be loaded lazily when needed for search
411+ let bundleId : String ? = nil
425412
426413 let plugin = PluginInfo (
427414 name: name,
@@ -433,7 +420,17 @@ struct PluginsView: View {
433420 bundleId: bundleId,
434421 customIcon: nil
435422 )
436- categoryPlugins. append ( plugin)
423+ batchBuffer. append ( plugin)
424+
425+ // Update UI every batchSize plugins for incremental loading
426+ if batchBuffer. count >= batchSize {
427+ let pluginsToAdd = batchBuffer
428+ batchBuffer = [ ]
429+
430+ await MainActor . run {
431+ self . allPlugins. append ( contentsOf: pluginsToAdd)
432+ }
433+ }
437434 }
438435 }
439436 } catch {
@@ -447,26 +444,40 @@ struct PluginsView: View {
447444 }
448445 }
449446
450- // #if DEBUG
451- // // Duplicate ZoomAudioDevice.driver 599 more times for performance testing
452- // if let zoomPlugin = categoryPlugins.first(where: { $0.name == "ZoomAudioDevice.driver" }) {
453- // for i in 1...599 {
454- // let duplicatedPlugin = PluginInfo(
455- // name: "\(zoomPlugin.name) (Copy \(i))",
456- // path: "\(zoomPlugin.path)_copy\(i)", // Fake path
457- // category: zoomPlugin.category,
458- // isDirectory: zoomPlugin.isDirectory,
459- // size: zoomPlugin.size,
460- // dateModified: zoomPlugin.dateModified,
461- // bundleId: zoomPlugin.bundleId.map { "\($0).copy\(i)" },
462- // customIcon: zoomPlugin.customIcon
463- // )
464- // categoryPlugins.append(duplicatedPlugin)
465- // }
466- // }
467- // #endif
468-
469- return ( category, categoryPlugins)
447+ // Flush any remaining plugins in the batch buffer
448+ if !batchBuffer. isEmpty {
449+ await MainActor . run {
450+ self . allPlugins. append ( contentsOf: batchBuffer)
451+ }
452+ }
453+
454+ #if DEBUG
455+ // Duplicate ZoomAudioDevice.driver 599 more times for performance testing
456+ if let zoomPlugin = self . allPlugins. first ( where: { $0. name == " ZoomAudioDevice.driver " && $0. category == category } ) {
457+ var debugPlugins : [ PluginInfo ] = [ ]
458+ for i in 1 ... 599 {
459+ let duplicatedPlugin = PluginInfo (
460+ name: " \( zoomPlugin. name) (Copy \( i) ) " ,
461+ path: " \( zoomPlugin. path) _copy \( i) " , // Fake path
462+ category: zoomPlugin. category,
463+ isDirectory: zoomPlugin. isDirectory,
464+ size: zoomPlugin. size,
465+ dateModified: zoomPlugin. dateModified,
466+ bundleId: zoomPlugin. bundleId. map { " \( $0) .copy \( i) " } ,
467+ customIcon: zoomPlugin. customIcon
468+ )
469+ debugPlugins. append ( duplicatedPlugin)
470+ }
471+
472+ // Add debug plugins in batches for smooth UI
473+ let debugBatches = debugPlugins. chunked ( into: batchSize)
474+ for batch in debugBatches {
475+ await MainActor . run {
476+ self . allPlugins. append ( contentsOf: batch)
477+ }
478+ }
479+ }
480+ #endif
470481 }
471482
472483
@@ -759,7 +770,8 @@ struct PluginCategorySection: View {
759770 }
760771 }
761772 }
762- . transition ( . opacity. combined ( with: . slide) )
773+ . transition ( plugins. count > 50 ? . identity : . opacity)
774+
763775 }
764776 }
765777 }
@@ -1246,7 +1258,7 @@ struct AudioPluginRowView: View {
12461258 . padding ( . vertical, 4 )
12471259 }
12481260 }
1249- . transition ( pluginsCount > 50 ? . identity : . opacity. combined ( with : . slide ) )
1261+ . transition ( pluginsCount > 50 ? . identity : . opacity)
12501262 }
12511263 }
12521264 . background (
@@ -1303,8 +1315,17 @@ struct AudioPluginRowView: View {
13031315 // Build search filters - search for plugin name
13041316 let filters : [ FilterType ] = [ . name( . contains, pluginName) ]
13051317
1318+ // Lazily extract bundle ID only when search is clicked (if not already loaded)
1319+ var bundleIdToSearch : String ? = plugin. bundleId
1320+ if bundleIdToSearch == nil && plugin. isDirectory && plugin. path. hasSuffix ( " .driver " ) {
1321+ let infoPlistPath = plugin. path + " /Contents/Info.plist "
1322+ if let plistData = NSDictionary ( contentsOfFile: infoPlistPath) {
1323+ bundleIdToSearch = plistData [ " CFBundleIdentifier " ] as? String
1324+ }
1325+ }
1326+
13061327 // If bundle ID exists, also search for that
1307- if let bundleId = plugin . bundleId , !bundleId. isEmpty {
1328+ if let bundleId = bundleIdToSearch , !bundleId. isEmpty {
13081329 // Don't add bundle ID as a filter since we can only search for name
13091330 // Instead, we'll do two searches - one for plugin name, one for bundle ID
13101331 let bundleIdEngine = FileSearchEngine ( )
0 commit comments