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