using AuroraDesk.Presentation.ViewModels.Base;
using AuroraDesk.Presentation.Services;
using Microsoft.Extensions.Logging;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
namespace AuroraDesk.Presentation.ViewModels.Pages;
///
/// 图片浏览页面 ViewModel
/// 参考 IconsPageViewModel 实现真正的按需加载
///
/// 核心改进:
/// 1. 初始仅加载前 30 个 ImageMetadata
/// 2. 滚动接近底部时再加载下一批(每次 50 个)
/// 3. 只创建当前需要显示的元数据对象
/// 4. 对 10 万张图片,内存占用显著降低
///
public class ImageGalleryPageViewModel : RoutableViewModel
{
private string _selectedDirectory = string.Empty;
private ObservableCollection _images = new();
private ImageMetadata? _selectedImage;
private bool _isLoading;
private int _totalImageCount;
private string _statusMessage = "请选择一个包含图片的目录";
private CancellationTokenSource? _loadingCancellationTokenSource;
private bool _isLoadingMore; // 是否正在加载更多
private readonly ILogger? _logger;
// 数据层:只保存文件路径列表(List),不加载实际图片对象
private List _allFilePaths = new();
// 已加载的元数据数量(跟踪已加载数量)
private int _loadedMetadataCount = 0;
// 支持的图片格式
private static readonly string[] SupportedImageExtensions =
{
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".ico"
};
// 初始加载数量
private const int InitialBatchSize = 30;
// 每次加载更多时的批次大小
private const int IncrementalBatchSize = 50;
public ObservableCollection Images
{
get => _images;
set => this.RaiseAndSetIfChanged(ref _images, value);
}
public ImageMetadata? SelectedImage
{
get => _selectedImage;
set => this.RaiseAndSetIfChanged(ref _selectedImage, value);
}
public bool IsLoading
{
get => _isLoading;
set => this.RaiseAndSetIfChanged(ref _isLoading, value);
}
public string SelectedDirectory
{
get => _selectedDirectory;
set => this.RaiseAndSetIfChanged(ref _selectedDirectory, value);
}
public int TotalImageCount
{
get => _totalImageCount;
set => this.RaiseAndSetIfChanged(ref _totalImageCount, value);
}
public string StatusMessage
{
get => _statusMessage;
set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
// 命令
public ReactiveCommand SelectDirectoryCommand { get; }
public ReactiveCommand SelectImageCommand { get; }
///
/// 构造函数
///
public ImageGalleryPageViewModel(
IScreen hostScreen,
ILogger? logger = null)
: base(hostScreen, "ImageGallery")
{
_logger = logger;
// 创建命令
SelectDirectoryCommand = ReactiveCommand.CreateFromTask(SelectDirectoryAsync);
SelectImageCommand = ReactiveCommand.Create(SelectImage);
}
///
/// 选择目录(用户交互层)
///
private async Task SelectDirectoryAsync()
{
try
{
var app = Avalonia.Application.Current;
TopLevel? topLevel = null;
if (app?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
topLevel = desktop.MainWindow;
}
if (topLevel?.StorageProvider == null)
{
_logger?.LogWarning("无法获取 StorageProvider");
StatusMessage = "无法打开文件对话框";
return;
}
// 打开文件夹选择对话框
var folders = await topLevel.StorageProvider.OpenFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = "选择包含图片的文件夹",
AllowMultiple = false
});
if (folders.Count > 0 && folders[0].TryGetLocalPath() is { } path)
{
SelectedDirectory = path;
await LoadImagesFromDirectoryAsync(path);
}
}
catch (Exception ex)
{
_logger?.LogError(ex, "选择目录时发生错误");
StatusMessage = $"选择目录失败: {ex.Message}";
}
}
///
/// 从目录加载图片(数据加载层)
/// 1. 扫描所有图片文件路径
/// 2. 初始仅加载前 30 个 ImageMetadata
/// 3. 后续通过 LoadMoreImagesAsync 按需加载
///
private async Task LoadImagesFromDirectoryAsync(string directoryPath)
{
// 取消之前的加载任务
_loadingCancellationTokenSource?.Cancel();
_loadingCancellationTokenSource?.Dispose();
_loadingCancellationTokenSource = new CancellationTokenSource();
var cancellationToken = _loadingCancellationTokenSource.Token;
try
{
IsLoading = true;
StatusMessage = "正在扫描图片文件...";
Images.Clear();
SelectedImage = null;
_allFilePaths.Clear();
_loadedMetadataCount = 0;
TotalImageCount = 0;
_logger?.LogInformation("开始从目录加载图片: {Directory}", directoryPath);
if (!Directory.Exists(directoryPath))
{
StatusMessage = "目录不存在";
IsLoading = false;
return;
}
// 第一步:异步扫描目录,收集所有图片路径
await Task.Run(async () =>
{
try
{
await foreach (var filePath in EnumerateImageFilesAsync(directoryPath, cancellationToken))
{
if (cancellationToken.IsCancellationRequested)
break;
_allFilePaths.Add(filePath);
// 每找到1000个文件就更新一次UI
if (_allFilePaths.Count % 1000 == 0)
{
await UpdateStatusAsync($"已找到 {_allFilePaths.Count} 个图片文件...", _allFilePaths.Count);
await Task.Yield();
}
}
}
catch (OperationCanceledException)
{
_logger?.LogInformation("文件扫描已取消");
return;
}
}, cancellationToken);
if (cancellationToken.IsCancellationRequested)
{
return;
}
// 对文件路径按文件名排序(自然排序)
_allFilePaths.Sort(NaturalStringComparer.Instance);
TotalImageCount = _allFilePaths.Count;
_logger?.LogInformation("找到 {Count} 张图片", TotalImageCount);
if (TotalImageCount == 0)
{
await UpdateStatusAsync("未找到图片文件", isLoading: false);
return;
}
// 第二步:初始仅加载前 30 个 ImageMetadata(真正的按需加载)
await LoadMoreImagesAsync(InitialBatchSize);
_logger?.LogInformation("图片加载完成,初始加载: {Count} 张,总数: {Total}", _loadedMetadataCount, TotalImageCount);
// 初始加载完成后,延迟检查是否需要继续加载更多
// 等待UI布局完成后再检查,确保有足够内容填满视口
_ = Task.Run(async () =>
{
await Task.Delay(500);
if (_loadedMetadataCount < _allFilePaths.Count && !_isLoadingMore)
{
// 再次检查并加载更多,确保有足够内容填满视口
await LoadMoreImagesAsync(IncrementalBatchSize);
}
});
}
catch (OperationCanceledException)
{
_logger?.LogInformation("图片加载已取消");
await UpdateStatusAsync("加载已取消", isLoading: false);
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载图片时发生错误");
await UpdateStatusAsync($"加载图片失败: {ex.Message}", isLoading: false);
}
}
///
/// 加载更多图片(按需加载)
/// 参考 IconsPageViewModel 的流式加载策略
///
public async Task LoadMoreImagesAsync(int count = IncrementalBatchSize)
{
if (_isLoadingMore || _loadedMetadataCount >= _allFilePaths.Count)
return;
_isLoadingMore = true;
try
{
// 计算需要加载的文件路径范围
var startIndex = _loadedMetadataCount;
var endIndex = Math.Min(_loadedMetadataCount + count, _allFilePaths.Count);
var filePathsToLoad = _allFilePaths.Skip(startIndex).Take(endIndex - startIndex).ToList();
if (filePathsToLoad.Count == 0)
return;
_logger?.LogInformation("开始加载更多图片: {Start} - {End} (共 {Count} 张)",
startIndex, endIndex, filePathsToLoad.Count);
// 在后台线程创建元数据
var metadataList = await CreateImageMetadataAsync(filePathsToLoad, CancellationToken.None);
// 在 UI 线程批量添加
await Dispatcher.UIThread.InvokeAsync(() =>
{
var wasEmpty = _images.Count == 0;
foreach (var metadata in metadataList)
{
_images.Add(metadata);
// 触发缩略图加载(按需加载)
metadata.EnsureThumbnailLoaded();
}
// 只在第一次加载时触发 PropertyChanged
if (wasEmpty)
{
this.RaisePropertyChanged(nameof(Images));
}
_loadedMetadataCount = _images.Count;
IsLoading = false;
StatusMessage = $"已加载 {_loadedMetadataCount} / {TotalImageCount} 张图片";
}, DispatcherPriority.Background);
_logger?.LogInformation("加载更多图片完成: 已加载 {Loaded} / {Total}", _loadedMetadataCount, TotalImageCount);
}
catch (Exception ex)
{
_logger?.LogError(ex, "加载更多图片时发生错误");
}
finally
{
_isLoadingMore = false;
}
}
///
/// 异步枚举图片文件(流式处理)
///
private async IAsyncEnumerable EnumerateImageFilesAsync(
string directoryPath,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
{
var channel = Channel.CreateUnbounded();
var writer = channel.Writer;
_ = Task.Run(async () =>
{
try
{
var dirQueue = new Queue();
dirQueue.Enqueue(directoryPath);
while (dirQueue.Count > 0 && !cancellationToken.IsCancellationRequested)
{
var currentDir = dirQueue.Dequeue();
try
{
// 枚举当前目录的文件
foreach (var file in Directory.EnumerateFiles(currentDir, "*.*", SearchOption.TopDirectoryOnly))
{
if (cancellationToken.IsCancellationRequested)
return;
var ext = Path.GetExtension(file).ToLowerInvariant();
if (SupportedImageExtensions.Contains(ext))
{
await writer.WriteAsync(file, cancellationToken);
}
}
// 枚举子目录(递归)
foreach (var subDir in Directory.EnumerateDirectories(currentDir, "*", SearchOption.TopDirectoryOnly))
{
if (cancellationToken.IsCancellationRequested)
return;
dirQueue.Enqueue(subDir);
}
}
catch (UnauthorizedAccessException)
{
_logger?.LogWarning("无权限访问目录: {Directory}", currentDir);
}
catch (DirectoryNotFoundException)
{
// 跳过不存在的目录
}
catch (Exception ex)
{
_logger?.LogWarning(ex, "处理目录时出错: {Directory}", currentDir);
}
}
}
finally
{
writer.Complete();
}
}, cancellationToken);
// 流式返回找到的文件
await foreach (var filePath in channel.Reader.ReadAllAsync(cancellationToken))
{
yield return filePath;
}
}
///
/// 创建图片元数据(轻量数据结构)
///
private async Task> CreateImageMetadataAsync(
IEnumerable filePaths,
CancellationToken cancellationToken)
{
var filePathsList = filePaths.ToList();
var metadataList = new List(filePathsList.Count);
await Task.Run(() =>
{
foreach (var filePath in filePathsList)
{
if (cancellationToken.IsCancellationRequested)
break;
try
{
var fileInfo = new FileInfo(filePath);
if (fileInfo.Exists)
{
var metadata = new ImageMetadata
{
FilePath = filePath,
FileName = Path.GetFileName(filePath),
FileSize = fileInfo.Length,
LastModified = fileInfo.LastWriteTime
};
metadataList.Add(metadata);
}
}
catch
{
// 忽略文件信息获取失败
}
}
}, cancellationToken);
return metadataList;
}
private void SelectImage(ImageMetadata image)
{
if (image == null) return;
SelectedImage = image;
_logger?.LogInformation("选择图片: {FileName}", image.FileName);
}
///
/// 清理资源(取消正在进行的加载任务)
///
public void CancelLoading()
{
_loadingCancellationTokenSource?.Cancel();
_loadingCancellationTokenSource?.Dispose();
_loadingCancellationTokenSource = null;
}
///
/// 更新状态消息(UI 线程安全)
///
private async Task UpdateStatusAsync(
string message,
int? totalCount = null,
bool? isLoading = null)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
StatusMessage = message;
if (totalCount.HasValue)
TotalImageCount = totalCount.Value;
if (isLoading.HasValue)
IsLoading = isLoading.Value;
}, DispatcherPriority.Background);
}
}
///
/// 图片元数据(轻量数据结构)
/// 按照 images.md:只包含文件名、大小、修改时间
///
public class ImageMetadata : ReactiveObject
{
private string _filePath = string.Empty;
private string _fileName = string.Empty;
private long _fileSize;
private DateTime _lastModified;
private Bitmap? _thumbnailSource;
private bool _isLoadingThumbnail;
// 缩略图缓存服务(单例)
private static readonly ThumbnailCacheService _thumbnailCache = ThumbnailCacheService.Instance;
public string FilePath
{
get => _filePath;
set => this.RaiseAndSetIfChanged(ref _filePath, value);
}
public string FileName
{
get => _fileName;
set => this.RaiseAndSetIfChanged(ref _fileName, value);
}
public long FileSize
{
get => _fileSize;
set => this.RaiseAndSetIfChanged(ref _fileSize, value);
}
public DateTime LastModified
{
get => _lastModified;
set => this.RaiseAndSetIfChanged(ref _lastModified, value);
}
///
/// 缩略图源(异步加载)
/// 按照 images.md:后台线程池按需生成缩略图
///
public Bitmap? ThumbnailSource
{
get => _thumbnailSource;
private set => this.RaiseAndSetIfChanged(ref _thumbnailSource, value);
}
///
/// 是否正在加载缩略图
///
public bool IsLoadingThumbnail
{
get => _isLoadingThumbnail;
private set => this.RaiseAndSetIfChanged(ref _isLoadingThumbnail, value);
}
///
/// 确保缩略图已加载(按需加载)
/// 按照 images.md:优先渲染可见项的缩略图
///
public void EnsureThumbnailLoaded()
{
if (_thumbnailSource != null || _isLoadingThumbnail || string.IsNullOrEmpty(_filePath))
return;
_ = LoadThumbnailAsync();
}
///
/// 异步加载缩略图(使用 ThumbnailCacheService)
/// 按照 images.md:支持内存LRU缓存和磁盘缓存
///
private async Task LoadThumbnailAsync()
{
if (string.IsNullOrEmpty(_filePath) || !File.Exists(_filePath))
{
ThumbnailSource = null;
return;
}
IsLoadingThumbnail = true;
try
{
// 使用 ThumbnailCacheService 加载缩略图(自动处理内存+磁盘缓存)
var thumbnail = await _thumbnailCache.GetThumbnailAsync(_filePath);
// 在UI线程更新
await Dispatcher.UIThread.InvokeAsync(() =>
{
ThumbnailSource = thumbnail;
IsLoadingThumbnail = false;
});
}
catch
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
ThumbnailSource = null;
IsLoadingThumbnail = false;
});
}
}
///
/// 格式化文件大小
///
public string FormattedFileSize
{
get
{
if (_fileSize < 1024) return $"{_fileSize} B";
if (_fileSize < 1024 * 1024) return $"{_fileSize / 1024.0:F2} KB";
return $"{_fileSize / (1024.0 * 1024.0):F2} MB";
}
}
}
///
/// 自然字符串比较器(支持数字文件名排序)
///
internal class NaturalStringComparer : IComparer
{
public static readonly NaturalStringComparer Instance = new();
public int Compare(string? x, string? y)
{
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;
var fileNameX = Path.GetFileName(x);
var fileNameY = Path.GetFileName(y);
var fileNameCompare = CompareNatural(fileNameX, fileNameY);
if (fileNameCompare != 0)
return fileNameCompare;
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
}
private static int CompareNatural(string? x, string? y)
{
if (x == null && y == null) return 0;
if (x == null) return -1;
if (y == null) return 1;
int i = 0, j = 0;
while (i < x.Length && j < y.Length)
{
if (char.IsDigit(x[i]) && char.IsDigit(y[j]))
{
int numX = 0, numY = 0;
while (i < x.Length && char.IsDigit(x[i]))
{
numX = numX * 10 + (x[i] - '0');
i++;
}
while (j < y.Length && char.IsDigit(y[j]))
{
numY = numY * 10 + (y[j] - '0');
j++;
}
if (numX != numY)
return numX.CompareTo(numY);
}
else
{
int charCompare = char.ToLowerInvariant(x[i]).CompareTo(char.ToLowerInvariant(y[j]));
if (charCompare != 0)
return charCompare;
i++;
j++;
}
}
return x.Length.CompareTo(y.Length);
}
}