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;
}
}