@@ -41,6 +41,7 @@ const int _iconCacheVersion = 9;
4141const int _iconCacheCapacity = 64 ;
4242final LinkedHashMap <String , Uint8List ?> _iconCache =
4343 LinkedHashMap <String , Uint8List ?>();
44+ final Map <String , Future <Uint8List ?>> _iconInFlight = {};
4445
4546class _IconTask {
4647 final String path;
@@ -55,6 +56,14 @@ final Queue<_IconTask> _iconTaskQueue = Queue<_IconTask>();
5556int _activeIconIsolates = 0 ;
5657// Limit concurrent isolates to avoid creating too many DCs at once.
5758const int _maxIconIsolates = 3 ;
59+ final Queue <_IconTask > _mainIconTaskQueue = Queue <_IconTask >();
60+ bool _mainIconDrainScheduled = false ;
61+
62+ void _debugLog (String message) {
63+ if (kDebugMode) {
64+ debugPrint (message);
65+ }
66+ }
5867
5968math.Point <int > getPrimaryScreenSize () {
6069 final w = GetSystemMetrics (SM_CXSCREEN );
@@ -1065,6 +1074,8 @@ Uint8List? extractIcon(String filePath, {int size = 64}) {
10651074 // Try to locate the icon resource via shell, then extract a high-res icon via
10661075 // PrivateExtractIconsW (handles PNG-in-ICO as well). Fallback to SHGetFileInfo
10671076 // HICON if needed.
1077+ final comReady = _ensureComReady ();
1078+ try {
10681079 final desiredSize = size.clamp (16 , 256 );
10691080
10701081 final primaryKey = _cacheKeyForFile (filePath, desiredSize);
@@ -1125,14 +1136,12 @@ Uint8List? extractIcon(String filePath, {int size = 64}) {
11251136 calloc.free (pathPtr);
11261137 if (hr == 0 ) {
11271138 calloc.free (shFileInfo);
1128- _writeIconCache (primaryKey, null );
11291139 return null ;
11301140 }
11311141
11321142 final iconHandle = shFileInfo.ref.hIcon;
11331143 calloc.free (shFileInfo);
11341144 if (iconHandle == 0 ) {
1135- _writeIconCache (primaryKey, null );
11361145 return null ;
11371146 }
11381147
@@ -1145,6 +1154,11 @@ Uint8List? extractIcon(String filePath, {int size = 64}) {
11451154 : primaryKey;
11461155 _writeIconCache (finalKey, cachedValue);
11471156 return cachedValue;
1157+ } finally {
1158+ if (comReady) {
1159+ CoUninitialize ();
1160+ }
1161+ }
11481162}
11491163
11501164Uint8List ? _extractJumboIconPng (String filePath, int desiredSize) {
@@ -1639,7 +1653,11 @@ Future<Uint8List?> extractIconAsync(String filePath, {int size = 64}) {
16391653 final cached = _readIconCache (cacheKey);
16401654 if (cached.found) return Future .value (cached.value);
16411655
1656+ final existing = _iconInFlight[cacheKey];
1657+ if (existing != null ) return existing;
1658+
16421659 final completer = Completer <Uint8List ?>();
1660+ _iconInFlight[cacheKey] = completer.future;
16431661 _iconTaskQueue.add (_IconTask (filePath, desiredSize, cacheKey, completer));
16441662 _drainIconTasks ();
16451663 return completer.future;
@@ -1649,22 +1667,30 @@ void _drainIconTasks() {
16491667 while (_activeIconIsolates < _maxIconIsolates && _iconTaskQueue.isNotEmpty) {
16501668 final task = _iconTaskQueue.removeFirst ();
16511669 _activeIconIsolates++ ;
1670+ final path = task.path;
1671+ final size = task.size;
16521672
1653- Isolate . run < Uint8List ?>(() => extractIcon (task. path, size: task.size) )
1673+ _runIconIsolate ( path, size)
16541674 .then ((data) {
1655- // Fallback: if isolate extraction fails/returns empty, retry in main
1656- // isolate to avoid missing icons (may block briefly but recovers).
1657- Uint8List ? result = data;
1658- if (result == null || result.isEmpty) {
1659- result = extractIcon (task.path, size: task.size);
1675+ final result = (data == null || data.isEmpty) ? null : data;
1676+ if (result != null ) {
1677+ _writeIconCache (task.cacheKey, result);
1678+ task.completer.complete (result);
1679+ _iconInFlight.remove (task.cacheKey);
1680+ } else {
1681+ _debugLog (
1682+ 'icon isolate empty: ${task .path } size=${task .size }' ,
1683+ );
1684+ _mainIconTaskQueue.add (task);
1685+ _scheduleMainIconDrain ();
16601686 }
1661- _writeIconCache (task.cacheKey, result);
1662- task.completer.complete (result);
16631687 })
1664- .catchError ((_, __) {
1665- final result = extractIcon (task.path, size: task.size);
1666- _writeIconCache (task.cacheKey, result);
1667- task.completer.complete (result);
1688+ .catchError ((err, st) {
1689+ _debugLog (
1690+ 'icon isolate error: ${task .path } size=${task .size } err=$err \n $st ' ,
1691+ );
1692+ _mainIconTaskQueue.add (task);
1693+ _scheduleMainIconDrain ();
16681694 })
16691695 .whenComplete (() {
16701696 _activeIconIsolates-- ;
@@ -1673,6 +1699,33 @@ void _drainIconTasks() {
16731699 }
16741700}
16751701
1702+ void _scheduleMainIconDrain () {
1703+ if (_mainIconDrainScheduled) return ;
1704+ _mainIconDrainScheduled = true ;
1705+ Future <void >(() async {
1706+ while (_mainIconTaskQueue.isNotEmpty) {
1707+ final task = _mainIconTaskQueue.removeFirst ();
1708+ Uint8List ? result;
1709+ try {
1710+ result = extractIcon (task.path, size: task.size);
1711+ } catch (_) {
1712+ _debugLog (
1713+ 'icon main fallback error: ${task .path } size=${task .size }' ,
1714+ );
1715+ result = null ;
1716+ }
1717+ _writeIconCache (task.cacheKey, result);
1718+ if (! task.completer.isCompleted) {
1719+ task.completer.complete (result);
1720+ }
1721+ _iconInFlight.remove (task.cacheKey);
1722+ // Yield between tasks to keep UI responsive.
1723+ await Future <void >.delayed (const Duration (milliseconds: 8 ));
1724+ }
1725+ _mainIconDrainScheduled = false ;
1726+ });
1727+ }
1728+
16761729_IconCacheResult _readIconCache (String key) {
16771730 if (! _iconCache.containsKey (key)) return const _IconCacheResult (found: false );
16781731 return _IconCacheResult (found: true , value: _iconCache[key]);
@@ -1697,6 +1750,44 @@ String _cacheKeyForSystemIndex(int index, int size) =>
16971750String _cacheKeyForFile (String filePath, int size) =>
16981751 'v$_iconCacheVersion |file:${path .normalize (filePath )}|$size ' ;
16991752
1753+ bool _ensureComReady () {
1754+ final hr = CoInitializeEx (nullptr, COINIT_APARTMENTTHREADED );
1755+ return hr == S_OK || hr == S_FALSE ;
1756+ }
1757+
1758+ Uint8List ? _extractIconIsolate (String path, int size) {
1759+ return extractIcon (path, size: size);
1760+ }
1761+
1762+ Future <Uint8List ?> _runIconIsolate (String path, int size) async {
1763+ final port = ReceivePort ();
1764+ await Isolate .spawn (
1765+ _iconIsolateEntry,
1766+ < Object > [port.sendPort, path, size],
1767+ );
1768+ final message = await port.first;
1769+ port.close ();
1770+ if (message is TransferableTypedData ) {
1771+ return message.materialize ().asUint8List ();
1772+ }
1773+ if (message is Uint8List ) {
1774+ return message;
1775+ }
1776+ return null ;
1777+ }
1778+
1779+ void _iconIsolateEntry (List <Object > args) {
1780+ final sendPort = args[0 ] as SendPort ;
1781+ final path = args[1 ] as String ;
1782+ final size = args[2 ] as int ;
1783+ final bytes = _extractIconIsolate (path, size);
1784+ if (bytes == null || bytes.isEmpty) {
1785+ sendPort.send (null );
1786+ return ;
1787+ }
1788+ sendPort.send (TransferableTypedData .fromList ([bytes]));
1789+ }
1790+
17001791List <String > getClipboardFilePaths () {
17011792 if (! Platform .isWindows) return [];
17021793 if (OpenClipboard (NULL ) == 0 ) return [];
0 commit comments