diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs index 8644422f9f..d287eb41bf 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs @@ -5,13 +5,13 @@ using System; using System.Globalization; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using ManagedCommon; using Microsoft.UI.Dispatching; -using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media.Imaging; using Peek.Common.Extensions; using Peek.Common.Helpers; @@ -22,27 +22,42 @@ using Windows.Foundation; namespace Peek.FilePreviewer.Previewers { - public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer, IDisposable + public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer { - private static readonly EnumerationOptions _fileEnumOptions = new() { MatchType = MatchType.Win32, AttributesToSkip = 0, IgnoreInaccessible = true }; - private static readonly EnumerationOptions _directoryEnumOptions = new() { MatchType = MatchType.Win32, AttributesToSkip = FileAttributes.ReparsePoint, IgnoreInaccessible = true }; - private readonly DispatcherTimer _folderSizeDispatcherTimer = new(); - private ulong _folderSize; + /// + /// The number of files to scan between updates when calculating folder size. + /// + private const int FolderEnumerationChunkSize = 100; + + /// + /// The maximum view updates per second when enumerating a folder's contents. + /// + private const int MaxUpdateFps = 15; + + /// + /// The icon to display when a file or folder's thumbnail or icon could not be retrieved. + /// + private static readonly SvgImageSource DefaultIcon = new(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg")); + + /// + /// The options to use for the folder size enumeration. We recurse through all files and all subfolders. + /// + private static readonly EnumerationOptions FolderEnumerationOptions; [ObservableProperty] - private UnsupportedFilePreviewData preview = new UnsupportedFilePreviewData(); + private UnsupportedFilePreviewData preview = new(); [ObservableProperty] private PreviewState state; + static UnsupportedFilePreviewer() + { + FolderEnumerationOptions = new() { RecurseSubdirectories = true, AttributesToSkip = FileAttributes.ReparsePoint }; + } + public UnsupportedFilePreviewer(IFileSystemItem file) { - _folderSizeDispatcherTimer.Interval = TimeSpan.FromMilliseconds(500); - _folderSizeDispatcherTimer.Tick += FolderSizeDispatcherTimer_Tick; - Item = file; - Preview.FileName = file.Name; - Preview.DateModified = file.DateModified?.ToString(CultureInfo.CurrentCulture); Dispatcher = DispatcherQueue.GetForCurrentThread(); } @@ -50,41 +65,40 @@ namespace Peek.FilePreviewer.Previewers private DispatcherQueue Dispatcher { get; } - private Task? IconPreviewTask { get; set; } - - private Task? DisplayInfoTask { get; set; } - - public void Dispose() - { - _folderSizeDispatcherTimer.Tick -= FolderSizeDispatcherTimer_Tick; - GC.SuppressFinalize(this); - } - - public Task GetPreviewSizeAsync(CancellationToken cancellationToken) - { - Size? size = new Size(680, 500); - var previewSize = new PreviewSize { MonitorSize = size, UseEffectivePixels = true }; - return Task.FromResult(previewSize); - } + public Task GetPreviewSizeAsync(CancellationToken cancellationToken) => + Task.FromResult(new PreviewSize { MonitorSize = new Size(680, 500), UseEffectivePixels = true }); public async Task LoadPreviewAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - State = PreviewState.Loading; - - IconPreviewTask = LoadIconPreviewAsync(cancellationToken); - DisplayInfoTask = LoadDisplayInfoAsync(cancellationToken); - - await Task.WhenAll(IconPreviewTask, DisplayInfoTask); - - if (HasFailedLoadingPreview()) + try { - State = PreviewState.Error; + await Dispatcher.RunOnUiThread(async () => + { + Preview.FileName = Item.Name; + Preview.DateModified = Item.DateModified?.ToString(CultureInfo.CurrentCulture); + + State = PreviewState.Loaded; + + await LoadIconPreviewAsync(cancellationToken); + }); + + var progress = new Progress(update => + { + Dispatcher.TryEnqueue(() => + { + Preview.FileSize = update; + }); + }); + + await LoadDisplayInfoAsync(progress, cancellationToken); } - else + catch (OperationCanceledException) { - State = PreviewState.Loaded; + } + catch (Exception ex) + { + Logger.LogError("UnsupportedFilePreviewer error.", ex); + State = PreviewState.Error; } } @@ -97,139 +111,59 @@ namespace Peek.FilePreviewer.Previewers }); } - public async Task LoadIconPreviewAsync(CancellationToken cancellationToken) + private async Task LoadIconPreviewAsync(CancellationToken cancellationToken) { - bool isIconValid = false; - - var isTaskSuccessful = await TaskExtension.RunSafe(async () => - { - cancellationToken.ThrowIfCancellationRequested(); - await Dispatcher.RunOnUiThread(async () => - { - cancellationToken.ThrowIfCancellationRequested(); - - var iconBitmap = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) - ?? await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken); - - cancellationToken.ThrowIfCancellationRequested(); - - isIconValid = iconBitmap != null; - - Preview.IconPreview = iconBitmap ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg")); - }); - }); - - return isIconValid && isTaskSuccessful; + Preview.IconPreview = await ThumbnailHelper.GetThumbnailAsync(Item.Path, cancellationToken) ?? + await ThumbnailHelper.GetIconAsync(Item.Path, cancellationToken) ?? + DefaultIcon; } - public async Task LoadDisplayInfoAsync(CancellationToken cancellationToken) + private async Task LoadDisplayInfoAsync(IProgress sizeProgress, CancellationToken cancellationToken) { - bool isDisplayValid = false; + string type = await Item.GetContentTypeAsync(); - var isTaskSuccessful = await TaskExtension.RunSafe(async () => + Dispatcher.TryEnqueue(() => Preview.FileType = type); + + if (Item is FolderItem folderItem) { - cancellationToken.ThrowIfCancellationRequested(); - - var type = await Task.Run(Item.GetContentTypeAsync); - - cancellationToken.ThrowIfCancellationRequested(); - - isDisplayValid = type != null; - - var readableFileSize = string.Empty; - - if (Item is FileItem) - { - readableFileSize = ReadableStringHelper.BytesToReadableString(Item.FileSizeBytes); - } - else if (Item is FolderItem) - { - ComputeFolderSize(cancellationToken); - } - - await Dispatcher.RunOnUiThread(() => - { - Preview.FileSize = readableFileSize; - Preview.FileType = type; - return Task.CompletedTask; - }); - }); - - return isDisplayValid && isTaskSuccessful; - } - - private bool HasFailedLoadingPreview() - { - var isLoadingIconPreviewSuccessful = IconPreviewTask?.Result ?? false; - var isLoadingDisplayInfoSuccessful = DisplayInfoTask?.Result ?? false; - - return !isLoadingIconPreviewSuccessful || !isLoadingDisplayInfoSuccessful; - } - - private void ComputeFolderSize(CancellationToken cancellationToken) - { - Task.Run( - async () => - { - try - { - // Special folders like recycle bin don't have a path - if (string.IsNullOrWhiteSpace(Item.Path)) - { - return; - } - - await Dispatcher.RunOnUiThread(_folderSizeDispatcherTimer.Start); - GetDirectorySize(new DirectoryInfo(Item.Path), cancellationToken); - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - Logger.LogError("Failed to calculate folder size", ex); - } - finally - { - await Dispatcher.RunOnUiThread(_folderSizeDispatcherTimer.Stop); - } - - // If everything went well, ensure the UI is updated - await Dispatcher.RunOnUiThread(UpdateFolderSize); - }, - cancellationToken); - } - - private void GetDirectorySize(DirectoryInfo directory, CancellationToken cancellationToken) - { - var files = directory.GetFiles("*", _fileEnumOptions); - for (var i = 0; i < files.Length; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var f = files[i]; - if (f.Length > 0) - { - _folderSize += Convert.ToUInt64(f.Length); - } + await Task.Run(() => CalculateFolderSizeWithProgress(Item.Path, sizeProgress, cancellationToken), cancellationToken); } - - var directories = directory.GetDirectories("*", _directoryEnumOptions); - for (var i = 0; i < directories.Length; i++) + else { - cancellationToken.ThrowIfCancellationRequested(); - GetDirectorySize(directories[i], cancellationToken); + ReportProgress(sizeProgress, Item.FileSizeBytes); } } - private void UpdateFolderSize() + private void CalculateFolderSizeWithProgress(string path, IProgress progress, CancellationToken cancellationToken) { - Preview.FileSize = ReadableStringHelper.BytesToReadableString(_folderSize); + ulong folderSize = 0; + TimeSpan updateInterval = TimeSpan.FromMilliseconds(1000 / MaxUpdateFps); + DateTime nextUpdate = DateTime.UtcNow + updateInterval; + + var files = new DirectoryInfo(path).EnumerateFiles("*", FolderEnumerationOptions); + + foreach (var chunk in files.Chunk(FolderEnumerationChunkSize)) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (DateTime.Now >= nextUpdate) + { + ReportProgress(progress, folderSize); + nextUpdate = DateTime.UtcNow + updateInterval; + } + + foreach (var file in chunk) + { + folderSize += (ulong)file.Length; + } + } + + ReportProgress(progress, folderSize); } - private void FolderSizeDispatcherTimer_Tick(object? sender, object e) + private void ReportProgress(IProgress progress, ulong size) { - UpdateFolderSize(); + progress.Report(ReadableStringHelper.BytesToReadableString(size)); } } }