Initial commit: FluidExplorer:从零实现的 WinUI 3 文件资源管理器替代品(Mica、命令栏、面包屑)
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
using FluidExplorer.Models;
|
||||
using FluidExplorer.Services.Icons;
|
||||
using FluidExplorer.ViewModels;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace FluidExplorer.Services.ItemVisuals;
|
||||
|
||||
/// <summary>
|
||||
/// 列表里每一行的图标/缩略图按需加载:只为真正可见的行发请求,
|
||||
/// 内置并发上限与去重,滚动再快也不会把外壳图标接口打爆。
|
||||
/// </summary>
|
||||
public sealed class ItemVisualService(IIconService icons, DispatcherQueue ui)
|
||||
{
|
||||
/// <summary>诊断开关:FLUID_DISABLE_ICONS=1 时完全跳过外壳取图(用于隔离图标路径引发的问题)。</summary>
|
||||
private static readonly bool IconsDisabled = Environment.GetEnvironmentVariable("FLUID_DISABLE_ICONS") == "1";
|
||||
|
||||
private static readonly HashSet<string> 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<string> _inFlight = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly object _sync = new();
|
||||
|
||||
public bool IsThumbnailCandidate(string extension) => ThumbnailExtensions.Contains(extension);
|
||||
|
||||
/// <summary>为一行请求小图标(列表/详情视图)。</summary>
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>为一行请求缩略图(网格视图);失败则保留图标。</summary>
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>导航离开时清掉去重表(已缓存的位图仍在图标服务里)。</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user