using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using NLog; using SFML.Audio; using Spine; using Spine.Exporters; using SpineViewer.Extensions; using SpineViewer.Models; using SpineViewer.Resources; using SpineViewer.Services; using SpineViewer.ViewModels; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shell; namespace SpineViewer.ViewModels { public class ExplorerListViewModel : ObservableObject { /// /// 预览图的保存质量 /// public static int PreviewQuality { get; set; } = 80; /// /// 缩略图文件名格式字符串, 需要一个参数 /// public static string PreviewFileNameFormat => ".{0}.preview.webp"; private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly MainWindowViewModel _vmMain; /// /// 当前目录路径 /// private string? _currentDirectory; /// /// 当前目录下文件项缓存 /// private readonly List _items = []; public ExplorerListViewModel(MainWindowViewModel vmMain) { _vmMain = vmMain; } /// /// 筛选字符串 /// public string? FilterString { get => _filterString; set { if (!SetProperty(ref _filterString, value)) return; if (string.IsNullOrWhiteSpace(_filterString)) { _shownItems = _items.ToList(); } else { _shownItems = []; _shownItems.AddRange(_items.Where(it => it.FileName.Contains(_filterString))); } OnPropertyChanged(nameof(ShownItems)); } } private string? _filterString; /// /// 当前目录下的所有子项文件, 含递归目录 /// public List ShownItems => _shownItems; private List _shownItems = []; /// /// 选择项, 显示某一项的具体信息和预览图 /// public ExplorerItemViewModel? SelectedItem => _selectedItem; private ExplorerItemViewModel? _selectedItem; /// /// 选择文件夹命令 /// public RelayCommand Cmd_ChangeCurrentDirectory => _cmd_ChangeCurrentDirectory ??= new(() => { if (OpenFolderService.OpenFolder(out var selectedPath)) { _currentDirectory = selectedPath; RefreshItems(); } }); private RelayCommand? _cmd_ChangeCurrentDirectory; public RelayCommand Cmd_RefreshItems => _cmd_RefreshItems ??= new(RefreshItems); private RelayCommand? _cmd_RefreshItems; /// /// 选中项发生变化命令 /// public RelayCommand Cmd_SelectionChanged => _cmd_SelectionChanged ??= new(args => { if (args is null || args.Count != 1) { SetProperty(ref _selectedItem, null, nameof(SelectedItem)); } else { SetProperty(ref _selectedItem, args[0] as ExplorerItemViewModel, nameof(SelectedItem)); } }); private RelayCommand? _cmd_SelectionChanged; /// /// 右键菜单, 添加到模型列表 /// public RelayCommand Cmd_AddSelectedItems => _cmd_AddSelectedItems ??= new(AddSelectedItems_Execute, args => args is not null && args.Count > 0); private RelayCommand? _cmd_AddSelectedItems; private void AddSelectedItems_Execute(IList? args) { if (args is null || args.Count <= 0) return; _vmMain.SpineObjectListViewModel.AddSpineObjectFromFileList(args.Cast().Select(m => m.FullPath).ToArray()); } /// /// 对参数项生成预览图 /// public RelayCommand Cmd_GeneratePreviews => _cmd_GeneratePreviews ??= new(GeneratePreview_Execute, args => args is not null && args.Count > 0); private RelayCommand? _cmd_GeneratePreviews; private void GeneratePreview_Execute(IList? args) { if (args is null || args.Count <= 0) return; if (args.Count <= 1) { var m = (ExplorerItemViewModel)args[0]; try { using var sp = new SpineObject(m.FullPath); sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h); var bounds = new SFML.Graphics.FloatRect(x, y, w, h).GetCanvasBounds(new(510, 510), 2); using var exporter = new FrameExporter(512, 512) { Center = bounds.Position + bounds.Size / 2, Size = new(bounds.Width, -bounds.Height), Format = SkiaSharp.SKEncodedImageFormat.Webp, Quality = PreviewQuality, }; exporter.Export(m.PreviewFilePath, sp); } catch (Exception ex) { _logger.Trace(ex.ToString()); _logger.Error("Failed to generate preview: {0}, {1}", m.PreviewFilePath, ex.Message); } _logger.LogCurrentProcessMemoryUsage(); } else { ProgressService.RunAsync((pr, ct) => GeneratePreviewTask( args.Cast().ToArray(), pr, ct), AppResource.Str_GeneratePreviewsTitle ); } } private void GeneratePreviewTask(ExplorerItemViewModel[] models, IProgressReporter reporter, CancellationToken ct) { int totalCount = models.Length; int success = 0; int error = 0; _vmMain.ProgressState = TaskbarItemProgressState.Normal; _vmMain.ProgressValue = 0; reporter.Total = totalCount; reporter.Done = 0; reporter.ProgressText = $"[0/{totalCount}]"; using var exporter = new FrameExporter(512, 512) { Format = SkiaSharp.SKEncodedImageFormat.Webp, Quality = PreviewQuality, }; for (int i = 0; i < totalCount; i++) { if (ct.IsCancellationRequested) break; var m = models[i]; reporter.ProgressText = $"[{i}/{totalCount}] {m.FullPath}"; try { using var sp = new SpineObject(m.FullPath); sp.Skeleton.GetBounds(out var x, out var y, out var w, out var h); var bounds = new SFML.Graphics.FloatRect(x, y, w, h).GetCanvasBounds(new(510, 510), 2); exporter.Center = bounds.Position + bounds.Size / 2; exporter.Size = new(bounds.Width, -bounds.Height); exporter.Export(m.PreviewFilePath, sp); success++; } catch (Exception ex) { _logger.Trace(ex.ToString()); _logger.Error("Failed to generate preview: {0}, {1}", m.PreviewFilePath, ex.Message); error++; } reporter.Done = i + 1; reporter.ProgressText = $"[{i + 1}/{totalCount}] {m}"; _vmMain.ProgressValue = (i + 1f) / totalCount; } _vmMain.ProgressState = TaskbarItemProgressState.None; if (error > 0) _logger.Warn("Preview generation {0} successfully, {1} failed", success, error); else _logger.Info("{0} previews generated successfully", success); _logger.LogCurrentProcessMemoryUsage(); } /// /// 删除参数项的预览图 /// public RelayCommand Cmd_DeletePreviews => _cmd_DeletePreviews ??= new(DeletePreview_Execute, args => args is not null && args.Count > 0); private RelayCommand? _cmd_DeletePreviews; private void DeletePreview_Execute(IList? args) { if (args is null || args.Count <= 0) return; if (!MessagePopupService.Quest(string.Format(AppResource.Str_DeleteItemsQuest, args.Count))) return; if (args.Count <= 10) { foreach (var m in args.Cast()) { try { File.Delete(m.PreviewFilePath); } catch (Exception ex) { _logger.Trace(ex.ToString()); _logger.Error("Failed to delete preview: {0}, {1}", m.PreviewFilePath, ex.Message); } } _logger.LogCurrentProcessMemoryUsage(); } else { ProgressService.RunAsync((pr, ct) => DeletePreviewTask( args.Cast().ToArray(), pr, ct), AppResource.Str_DeletePreviewsTitle ); } } private void DeletePreviewTask(ExplorerItemViewModel[] models, IProgressReporter reporter, CancellationToken ct) { int totalCount = models.Length; int success = 0; int error = 0; _vmMain.ProgressState = TaskbarItemProgressState.Normal; _vmMain.ProgressValue = 0; reporter.Total = totalCount; reporter.Done = 0; reporter.ProgressText = $"[0/{totalCount}]"; for (int i = 0; i < totalCount; i++) { if (ct.IsCancellationRequested) break; var m = models[i]; reporter.ProgressText = $"[{i}/{totalCount}] {m.FullPath}"; try { File.Delete(m.PreviewFilePath); success++; } catch (Exception ex) { _logger.Trace(ex.ToString()); _logger.Error("Failed to delete preview: {0}, {1}", m.PreviewFilePath, ex.Message); error++; } reporter.Done = i + 1; reporter.ProgressText = $"[{i + 1}/{totalCount}] {m}"; _vmMain.ProgressValue = (i + 1f) / totalCount; } _vmMain.ProgressState = TaskbarItemProgressState.None; if (error > 0) _logger.Warn("Preview deletion {0} successfully, {1} failed", success, error); else _logger.Info("{0} previews deleted successfully", success); _logger.LogCurrentProcessMemoryUsage(); } /// /// 刷新显示, 可以更新文件夹项缓存 /// public void RefreshItems() { _items.Clear(); if (Directory.Exists(_currentDirectory)) { try { foreach (var file in Directory.EnumerateFiles(_currentDirectory, "*.*", SearchOption.AllDirectories)) { var lowerPath = file.ToLower(); if (SpineObject.PossibleSuffixMapping.Keys.Any(lowerPath.EndsWith)) _items.Add(new(file)); } } catch (Exception ex) { _logger.Trace(ex.ToString()); _logger.Error("Failed to enumerate files in dir: {0}, {1}", _currentDirectory, ex.Message); } } _shownItems = []; if (string.IsNullOrWhiteSpace(_filterString)) { _shownItems = _items.ToList(); } else { _shownItems = []; _shownItems.AddRange(_items.Where(it => it.FileName.Contains(_filterString))); } OnPropertyChanged(nameof(ShownItems)); } } public class ExplorerItemViewModel : ObservableObject { private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); public ExplorerItemViewModel(string path) { FullPath = Path.GetFullPath(path); FileDirectory = Path.GetDirectoryName(FullPath) ?? ""; FileName = Path.GetFileName(FullPath); PreviewFilePath = Path.Combine(FileDirectory, string.Format(ExplorerListViewModel.PreviewFileNameFormat, FileName)); } /// /// 完整路径 /// /// 文件所处目录 /// public string FileDirectory { get; } /// /// 文件名 /// public string FileName { get; } /// /// 预览图路径 /// public string PreviewFilePath { get; } /// /// 预览图 /// public ImageSource? PreviewImage { get { try { return WpfExtension.LoadWebpWithAlpha(PreviewFilePath); } catch (FileNotFoundException) { return null; } catch (Exception ex) { _logger.Trace(ex.ToString()); _logger.Warn("Failed to load preview image for {0}, {1}", FullPath, ex.Message); return null; } } } } }