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; } /// /// 清理所有缩略图引用(在 tab 切换或视图停用时调用) /// 防止在视图切换时访问已释放的 Bitmap /// public void ClearThumbnailReferences() { // 在 UI 线程上清理所有缩略图引用 Dispatcher.UIThread.Post(() => { foreach (var image in _images) { // 将 ThumbnailSource 设为 null,但不释放 Bitmap(由缓存服务管理) image.ClearThumbnailReference(); } }, DispatcherPriority.Background); } /// /// 重置所有缩略图引用并尝试从缓存恢复(在视图重新激活时调用) /// 性能优化:只恢复可见区域附近的图片,其他图片按需加载 /// /// 可见区域的起始索引(可选,如果为-1则只恢复前100个) /// 可见区域的结束索引(可选,如果为-1则只恢复前100个) public void ResetAllThumbnailReferences(int visibleStartIndex = -1, int visibleEndIndex = -1) { // 在 UI 线程上重置所有缩略图引用 Dispatcher.UIThread.Post(() => { var imagesList = _images.ToList(); // 创建副本,避免在迭代时集合被修改 // 确定需要立即恢复的范围 int startIndex, endIndex; if (visibleStartIndex >= 0 && visibleEndIndex >= 0 && visibleStartIndex < imagesList.Count) { // 恢复可见区域附近的图片(前后各扩展100个,确保预加载) startIndex = Math.Max(0, visibleStartIndex - 100); endIndex = Math.Min(imagesList.Count, visibleEndIndex + 100); } else { // 如果没有提供可见区域信息,只恢复前100个(默认情况) startIndex = 0; endIndex = Math.Min(100, imagesList.Count); } // 先重置所有标志位(不触发恢复,性能优化) foreach (var image in imagesList) { image.ResetThumbnailReferenceClearedFlagOnly(); } // 只恢复可见区域附近的图片(立即恢复) for (int i = startIndex; i < endIndex; i++) { imagesList[i].ResetThumbnailReferenceCleared(); } // 其他图片会在 UI 绑定时按需加载(通过 EnsureThumbnailLoaded) }, DispatcherPriority.Loaded); } /// /// 更新状态消息(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 bool _thumbnailReferenceCleared = false; // 标记缩略图引用是否已被清理 // 缩略图缓存服务(单例) 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:后台线程池按需生成缩略图 /// 注意:这里持有的是缓存中 Bitmap 的引用,不负责释放资源 /// 性能优化:不再在 getter 中检查 PixelSize(大量图片时性能开销太大), /// 而是通过 _thumbnailReferenceCleared 标志和视图生命周期管理来确保安全 /// public Bitmap? ThumbnailSource { get { // 如果引用已被清理,直接返回 null(性能优化:避免访问 PixelSize) if (_thumbnailReferenceCleared) return null; return _thumbnailSource; } private set { // 设置新值时,重置清理标志 if (value != null) { _thumbnailReferenceCleared = false; } // 设置新值前,旧值由缓存服务管理,不需要手动释放 this.RaiseAndSetIfChanged(ref _thumbnailSource, value); } } /// /// 是否正在加载缩略图 /// public bool IsLoadingThumbnail { get => _isLoadingThumbnail; private set => this.RaiseAndSetIfChanged(ref _isLoadingThumbnail, value); } /// /// 确保缩略图已加载(按需加载) /// 按照 images.md:优先渲染可见项的缩略图 /// public void EnsureThumbnailLoaded() { // 如果引用已被清理,需要从缓存恢复 if (_thumbnailReferenceCleared) { // 如果 _thumbnailSource 为 null,说明需要从缓存恢复 if (_thumbnailSource == null) { _thumbnailReferenceCleared = false; _ = LoadThumbnailFromCacheAsync(); return; } else { // 如果 _thumbnailSource 不为 null,只是标志位被清理,直接重置标志 _thumbnailReferenceCleared = false; this.RaisePropertyChanged(nameof(ThumbnailSource)); return; } } 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"; } } /// /// 清理缩略图引用(不释放 Bitmap,由缓存服务管理) /// 在 tab 切换时调用,防止访问已释放的 Bitmap /// 性能优化:使用标志位而不是直接访问 Bitmap 属性 /// public void ClearThumbnailReference() { // 使用标志位标记引用已清理,getter 会返回 null // 设置 _thumbnailSource = null,在重新激活时会从缓存恢复 _thumbnailReferenceCleared = true; _thumbnailSource = null; this.RaisePropertyChanged(nameof(ThumbnailSource)); } /// /// 重置清理标志并尝试从缓存恢复缩略图(在视图重新激活时调用) /// public void ResetThumbnailReferenceCleared() { _thumbnailReferenceCleared = false; // 尝试从缓存恢复缩略图(异步,不阻塞) // 如果缓存中还有,直接使用;如果没有,会触发重新加载 if (string.IsNullOrEmpty(_filePath)) return; // 触发从缓存重新加载(异步,不阻塞) // 即使缓存中没有,也会触发重新生成 _ = LoadThumbnailFromCacheAsync(); } /// /// 只重置清理标志,不立即恢复缩略图(性能优化) /// 缩略图会在 UI 绑定时按需加载 /// public void ResetThumbnailReferenceClearedFlagOnly() { _thumbnailReferenceCleared = false; } /// /// 从缓存重新加载缩略图(如果缓存中还有) /// private async Task LoadThumbnailFromCacheAsync() { if (string.IsNullOrEmpty(_filePath) || _isLoadingThumbnail) return; try { // 尝试从缓存获取(如果缓存中还有,直接返回;如果没有,返回 null) var thumbnail = await _thumbnailCache.GetThumbnailAsync(_filePath); // 在UI线程更新 await Dispatcher.UIThread.InvokeAsync(() => { ThumbnailSource = thumbnail; // 如果缓存中没有,触发重新加载 if (thumbnail == null && !_isLoadingThumbnail) { _ = LoadThumbnailAsync(); } }); } catch { // 加载失败,触发重新生成 await Dispatcher.UIThread.InvokeAsync(() => { if (!_isLoadingThumbnail) { _ = LoadThumbnailAsync(); } }); } } } /// /// 自然字符串比较器(支持数字文件名排序) /// 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); } }