using FluidExplorer.Models; using FluidExplorer.Services.Icons; using FluidExplorer.ViewModels; using Microsoft.UI.Dispatching; namespace FluidExplorer.Services.ItemVisuals; /// /// 列表里每一行的图标/缩略图按需加载:只为真正可见的行发请求, /// 内置并发上限与去重,滚动再快也不会把外壳图标接口打爆。 /// public sealed class ItemVisualService(IIconService icons, DispatcherQueue ui) { /// 诊断开关:FLUID_DISABLE_ICONS=1 时完全跳过外壳取图(用于隔离图标路径引发的问题)。 private static readonly bool IconsDisabled = Environment.GetEnvironmentVariable("FLUID_DISABLE_ICONS") == "1"; private static readonly HashSet ThumbnailExtensions = new(StringComparer.OrdinalIgnoreCase) { "jpg", "jpeg", "png", "gif", "bmp", "webp", "tif", "tiff", "heic", "heif", "avif", "ico", "svg", "mp4", "mkv", "avi", "mov", "wmv", "m4v", "webm", "mpg", "mpeg", "ts", "pdf" }; private readonly SemaphoreSlim _iconGate = new(6, 6); private readonly SemaphoreSlim _thumbGate = new(3, 3); private readonly HashSet _inFlight = new(StringComparer.OrdinalIgnoreCase); private readonly object _sync = new(); public bool IsThumbnailCandidate(string extension) => ThumbnailExtensions.Contains(extension); /// 为一行请求小图标(列表/详情视图)。 public void RequestIcon(ExplorerItem item, int size = 32) { if (IconsDisabled) return; var key = "i|" + item.FullPath + "|" + size; if (!TryBegin(key)) return; _ = Task.Run(async () => { try { var source = await icons.GetIconAsync(item.FullPath, item.IsDirectory, size, CancellationToken.None).ConfigureAwait(false); if (source is null && !item.IsDirectory) source = await icons.GetExtensionIconAsync(item.Extension, size, CancellationToken.None).ConfigureAwait(false); if (source is null) return; ui.TryEnqueue(() => { try { item.Icon = source; } catch (Exception ex) { App.Log($"设置图标失败: {ex.Message}"); } }); } catch { // 图标失败不影响使用 } finally { _iconGate.Release(); } }); } /// 为一行请求缩略图(网格视图);失败则保留图标。 public void RequestThumbnail(ExplorerItem item, int size) { if (IconsDisabled) return; if (item.IsDirectory || !IsThumbnailCandidate(item.Extension)) { RequestIcon(item, Math.Min(size, 96)); return; } var key = "t|" + item.FullPath + "|" + size; if (!TryBeginThumb(key)) return; _ = Task.Run(async () => { try { var source = await icons.GetThumbnailAsync(item.FullPath, size, CancellationToken.None).ConfigureAwait(false); if (source is null) { source = await icons.GetIconAsync(item.FullPath, false, Math.Min(size, 96), CancellationToken.None).ConfigureAwait(false); if (source is null) return; ui.TryEnqueue(() => { try { item.Icon = source; } catch (Exception ex) { App.Log($"设置图标失败: {ex.Message}"); } }); return; } ui.TryEnqueue(() => { try { item.HasThumbnail = true; item.Icon = source; } catch (Exception ex) { App.Log($"设置缩略图失败: {ex.Message}"); } }); } catch { // 忽略 } finally { _thumbGate.Release(); } }); } public void RequestIcon(SearchResultItem item, int size = 32) { if (IconsDisabled) return; var key = "si|" + item.FullPath + "|" + size; if (!TryBegin(key)) return; _ = Task.Run(async () => { try { var source = await icons.GetIconAsync(item.FullPath, item.IsDirectory, size, CancellationToken.None).ConfigureAwait(false); if (source is null && !item.IsDirectory) source = await icons.GetExtensionIconAsync(item.Hit.Extension, size, CancellationToken.None).ConfigureAwait(false); if (source is null) return; ui.TryEnqueue(() => { try { item.Icon = source; } catch (Exception ex) { App.Log($"设置图标失败: {ex.Message}"); } }); } catch { // 忽略 } finally { _iconGate.Release(); } }); } /// 导航离开时清掉去重表(已缓存的位图仍在图标服务里)。 public void ResetPending() { lock (_sync) _inFlight.Clear(); } private bool TryBegin(string key) { lock (_sync) { if (!_inFlight.Add(key)) return false; } if (!_iconGate.Wait(0)) { lock (_sync) _inFlight.Remove(key); // 让给其它行,稍后由可见性变化再次触发 return false; } return true; } private bool TryBeginThumb(string key) { lock (_sync) { if (!_inFlight.Add(key)) return false; } if (!_thumbGate.Wait(0)) { lock (_sync) _inFlight.Remove(key); return false; } return true; } }