diff --git a/src/Files.App/Data/Enums/IconOptions.cs b/src/Files.App/Data/Enums/IconOptions.cs index 0f4298637668..3fdba1d05a27 100644 --- a/src/Files.App/Data/Enums/IconOptions.cs +++ b/src/Files.App/Data/Enums/IconOptions.cs @@ -19,19 +19,34 @@ public enum IconOptions /// UseCurrentScale = 1, + /// + /// Scale the thumbnail to the requested size. + /// + ResizeThumbnail = 2, + /// /// Retrieve only the file icon, even a thumbnail is available. This has the best performance. /// - ReturnIconOnly = 2, + ReturnIconOnly = 4, /// /// Retrieve only the thumbnail. /// - ReturnThumbnailOnly = 4, + ReturnThumbnailOnly = 8, /// /// Retrieve a thumbnail only if it is cached or embedded in the file. /// - ReturnOnlyIfCached = 8, + ReturnOnlyIfCached = 16, + + /// + /// Default. Retrieve a thumbnail to display a preview of any single item (like a file, folder, or file group). + /// + SingleItem = 32, + + /// + /// Retrieve a thumbnail to display previews of files (or other items) in a list. + /// + ListView = 64, } } diff --git a/src/Files.App/Data/Items/DriveItem.cs b/src/Files.App/Data/Items/DriveItem.cs index 6c72c3769c41..0d9086d1b8d9 100644 --- a/src/Files.App/Data/Items/DriveItem.cs +++ b/src/Files.App/Data/Items/DriveItem.cs @@ -6,15 +6,15 @@ using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media.Imaging; using Windows.Storage; -using Windows.Storage.Streams; +using Windows.Storage.FileProperties; using ByteSize = ByteSizeLib.ByteSize; namespace Files.App.Data.Items { public sealed class DriveItem : ObservableObject, INavigationControlItem, ILocatableFolder { - private BitmapImage icon; - public BitmapImage Icon + private BitmapImage? icon; + public BitmapImage? Icon { get => icon; set @@ -24,7 +24,7 @@ public BitmapImage Icon } } - public byte[] IconData { get; set; } + public byte[]? IconData { get; set; } private string path; public string Path @@ -232,12 +232,11 @@ private void ItemDecorator_Click(object sender, RoutedEventArgs e) DriveHelpers.EjectDeviceAsync(Path); } - public static async Task CreateFromPropertiesAsync(StorageFolder root, string deviceId, string label, DriveType type, IRandomAccessStream imageStream = null) + public static async Task CreateFromPropertiesAsync(StorageFolder root, string deviceId, string label, DriveType type, byte[]? imageData = null) { var item = new DriveItem(); - if (imageStream is not null) - item.IconData = await imageStream.ToByteArrayAsync(); + item.IconData = imageData; item.Text = type switch { @@ -336,8 +335,7 @@ public async Task LoadThumbnailAsync() if (Root is not null) { - using var thumbnail = await DriveHelpers.GetThumbnailAsync(Root); - IconData ??= thumbnail is not null ? await thumbnail.ToByteArrayAsync() : null; + IconData ??= await FileThumbnailHelper.GetIconAsync(Root, 40, ThumbnailMode.SingleItem, ThumbnailOptions.UseCurrentScale); } if (string.Equals(DeviceID, "network-folder")) diff --git a/src/Files.App/Data/Items/WidgetDriveCardItem.cs b/src/Files.App/Data/Items/WidgetDriveCardItem.cs index 08e3d1136831..06efcae63910 100644 --- a/src/Files.App/Data/Items/WidgetDriveCardItem.cs +++ b/src/Files.App/Data/Items/WidgetDriveCardItem.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.UI.Xaml.Media.Imaging; +using Windows.Storage.FileProperties; namespace Files.App.Data.Items { @@ -34,8 +35,7 @@ public async Task LoadCardThumbnailAsync() if (result is null) { - using var thumbnail = await DriveHelpers.GetThumbnailAsync(Item.Root); - result ??= await thumbnail.ToByteArrayAsync(); + result ??= await FileThumbnailHelper.GetIconAsync(Item.Root, 40, ThumbnailMode.SingleItem, ThumbnailOptions.UseCurrentScale); } thumbnailData = result; diff --git a/src/Files.App/Data/Models/PinnedFoldersManager.cs b/src/Files.App/Data/Models/PinnedFoldersManager.cs index d2e84d1cd57a..4fb9036342e8 100644 --- a/src/Files.App/Data/Models/PinnedFoldersManager.cs +++ b/src/Files.App/Data/Models/PinnedFoldersManager.cs @@ -108,6 +108,7 @@ public async Task CreateLocationItemFromPathAsync(string path) { var result = await FileThumbnailHelper.GetIconAsync( res.Result.Path, + res.Result, Constants.ShellIconSizes.Small, true, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); diff --git a/src/Files.App/Services/Storage/StorageDevicesService.cs b/src/Files.App/Services/Storage/StorageDevicesService.cs index a13ab8d41a42..a336853c05a9 100644 --- a/src/Files.App/Services/Storage/StorageDevicesService.cs +++ b/src/Files.App/Services/Storage/StorageDevicesService.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using System.IO; using Windows.Storage; +using Windows.Storage.FileProperties; namespace Files.App.Services { @@ -42,7 +43,7 @@ public async IAsyncEnumerable GetDrivesAsync() continue; } - using var thumbnail = await DriveHelpers.GetThumbnailAsync(res.Result); + var thumbnail = await FileThumbnailHelper.GetIconAsync(res.Result, 40, ThumbnailMode.SingleItem, ThumbnailOptions.UseCurrentScale); var type = DriveHelpers.GetDriveType(drive); var label = DriveHelpers.GetExtendedDriveLabel(drive); var driveItem = await DriveItem.CreateFromPropertiesAsync(res.Result, drive.Name.TrimEnd('\\'), label, type, thumbnail); diff --git a/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs b/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs index 1a16a5351ecb..61a8d16f0b31 100644 --- a/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs +++ b/src/Files.App/Utils/Storage/Helpers/DriveHelpers.cs @@ -163,10 +163,5 @@ public static unsafe string GetExtendedDriveLabel(SystemIO.DriveInfo drive) }) ?? ""; } - - public static async Task GetThumbnailAsync(StorageFolder folder) - => (StorageItemThumbnail)await FilesystemTasks.Wrap(() - => folder.GetThumbnailAsync(ThumbnailMode.SingleItem, 40, ThumbnailOptions.UseCurrentScale).AsTask() - ); } } \ No newline at end of file diff --git a/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs b/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs index 72a838089f47..d037ebdd3a44 100644 --- a/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs +++ b/src/Files.App/Utils/Storage/Helpers/FileThumbnailHelper.cs @@ -1,20 +1,58 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using Windows.Storage; using Windows.Storage.FileProperties; namespace Files.App.Utils.Storage { public static class FileThumbnailHelper { + public static async Task GetIconAsync(string? path, IStorageItem? item, uint requestedSize, bool isFolder, IconOptions iconOptions) + { + byte[]? result = null; + if (path is not null) + result ??= await GetIconAsync(path, requestedSize, isFolder, iconOptions); + if (item is not null) + result ??= await GetIconAsync(item, requestedSize, iconOptions); + return result; + } + /// /// Returns icon or thumbnail for given file or folder /// - public static async Task GetIconAsync(string path, uint requestedSize, bool isFolder, IconOptions iconOptions) + public static Task GetIconAsync(string path, uint requestedSize, bool isFolder, IconOptions iconOptions) { var size = iconOptions.HasFlag(IconOptions.UseCurrentScale) ? requestedSize * App.AppModel.AppWindowDPI : requestedSize; - return await Win32Helper.StartSTATask(() => Win32Helper.GetIcon(path, (int)size, isFolder, iconOptions)); + return Win32Helper.StartSTATask(() => Win32Helper.GetIcon(path, (int)size, isFolder, iconOptions)); + } + + /// + /// Returns thumbnail for given file or folder using Storage API + /// + public static Task GetIconAsync(IStorageItem item, uint requestedSize, IconOptions iconOptions) + { + var thumbnailOptions = (iconOptions.HasFlag(IconOptions.UseCurrentScale) ? ThumbnailOptions.UseCurrentScale : 0) | + (iconOptions.HasFlag(IconOptions.ReturnOnlyIfCached) ? ThumbnailOptions.ReturnOnlyIfCached : 0) | + (iconOptions.HasFlag(IconOptions.ResizeThumbnail) ? ThumbnailOptions.ResizeThumbnail : 0); + + var thumbnailMode = iconOptions.HasFlag(IconOptions.ListView) ? ThumbnailMode.ListView : ThumbnailMode.SingleItem; + + return GetIconAsync(item, requestedSize, thumbnailMode, thumbnailOptions); + } + + public static async Task GetIconAsync(IStorageItem item, uint requestedSize, ThumbnailMode thumbnailMode, ThumbnailOptions thumbnailOptions) + { + using StorageItemThumbnail thumbnail = item switch + { + BaseStorageFile file => await FilesystemTasks.Wrap(() => file.GetThumbnailAsync(thumbnailMode, requestedSize, thumbnailOptions).AsTask()), + BaseStorageFolder folder => await FilesystemTasks.Wrap(() => folder.GetThumbnailAsync(thumbnailMode, requestedSize, thumbnailOptions).AsTask()), + _ => new(null!, FileSystemStatusCode.Generic) + }; + if (thumbnail is not null && thumbnail.Size != 0 && thumbnail.OriginalHeight != 0 && thumbnail.OriginalWidth != 0) + return await thumbnail.ToByteArrayAsync(); + return null; } /// @@ -23,14 +61,13 @@ public static class FileThumbnailHelper /// /// /// - public static async Task GetIconOverlayAsync(string path, bool isFolder) - => await Win32Helper.StartSTATask(() => Win32Helper.GetIconOverlay(path, isFolder)); + public static Task GetIconOverlayAsync(string path, bool isFolder) + => Win32Helper.StartSTATask(() => Win32Helper.GetIconOverlay(path, isFolder)); [Obsolete] - public static async Task LoadIconFromPathAsync(string filePath, uint thumbnailSize, ThumbnailMode thumbnailMode, ThumbnailOptions thumbnailOptions, bool isFolder = false) + public static Task LoadIconFromPathAsync(string filePath, uint thumbnailSize, ThumbnailMode thumbnailMode, ThumbnailOptions thumbnailOptions, bool isFolder = false) { - var result = await GetIconAsync(filePath, thumbnailSize, isFolder, IconOptions.None); - return result; + return GetIconAsync(filePath, thumbnailSize, isFolder, IconOptions.None); } } } \ No newline at end of file diff --git a/src/Files.App/Utils/Storage/Search/FolderSearch.cs b/src/Files.App/Utils/Storage/Search/FolderSearch.cs index 08df6e230def..940869a0abe8 100644 --- a/src/Files.App/Utils/Storage/Search/FolderSearch.cs +++ b/src/Files.App/Utils/Storage/Search/FolderSearch.cs @@ -512,6 +512,7 @@ private async Task GetListedItemAsync(IStorageItem item) { var iconResult = await FileThumbnailHelper.GetIconAsync( item.Path, + item, Constants.ShellIconSizes.Small, item.IsOfType(StorageItemTypes.Folder), IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); diff --git a/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs b/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs index 750ac63b8ed0..8fc2c45c5c07 100644 --- a/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/DriveProperties.cs @@ -52,6 +52,7 @@ public async override Task GetSpecialPropertiesAsync() { var result = await FileThumbnailHelper.GetIconAsync( Drive.Path, + diskRoot, Constants.ShellIconSizes.ExtraLarge, true, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); diff --git a/src/Files.App/ViewModels/Properties/Items/FileProperties.cs b/src/Files.App/ViewModels/Properties/Items/FileProperties.cs index d0de82a45d27..1d46b316b464 100644 --- a/src/Files.App/ViewModels/Properties/Items/FileProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/FileProperties.cs @@ -108,8 +108,13 @@ public override async Task GetSpecialPropertiesAsync() ViewModel.ItemSizeOnDisk = Win32Helper.GetFileSizeOnDisk(Item.ItemPath)?.ToLongSizeString() ?? string.Empty; + string filePath = (Item as ShortcutItem)?.TargetPath ?? Item.ItemPath; + BaseStorageFile? file = !string.IsNullOrWhiteSpace(filePath) ? + await AppInstance.ShellViewModel.GetFileFromPathAsync(filePath) : null!; + var result = await FileThumbnailHelper.GetIconAsync( Item.ItemPath, + file, Constants.ShellIconSizes.ExtraLarge, false, IconOptions.UseCurrentScale); @@ -132,9 +137,6 @@ public override async Task GetSpecialPropertiesAsync() } } - string filePath = (Item as ShortcutItem)?.TargetPath ?? Item.ItemPath; - BaseStorageFile file = await AppInstance.ShellViewModel.GetFileFromPathAsync(filePath); - // Couldn't access the file and can't load any other properties if (file is null) return; @@ -152,7 +154,7 @@ public override async Task GetSpecialPropertiesAsync() ViewModel.UncompressedItemSizeBytes = uncompressedSize; } - if (file.Properties is not null) + if (file?.Properties is not null) GetOtherPropertiesAsync(file.Properties); } diff --git a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs index 0dd47ef2610a..eec92284556c 100644 --- a/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs +++ b/src/Files.App/ViewModels/Properties/Items/FolderProperties.cs @@ -79,8 +79,13 @@ public async override Task GetSpecialPropertiesAsync() ViewModel.CanCompressContent = Win32Helper.CanCompressContent(Item.ItemPath); ViewModel.IsContentCompressed = Win32Helper.HasFileAttribute(Item.ItemPath, System.IO.FileAttributes.Compressed); + string folderPath = (Item as ShortcutItem)?.TargetPath ?? Item.ItemPath; + BaseStorageFolder? storageFolder = !string.IsNullOrWhiteSpace(folderPath) ? + await AppInstance.ShellViewModel.GetFolderFromPathAsync(folderPath) : null!; + var result = await FileThumbnailHelper.GetIconAsync( Item.ItemPath, + storageFolder, Constants.ShellIconSizes.ExtraLarge, true, IconOptions.UseCurrentScale); @@ -111,9 +116,6 @@ public async override Task GetSpecialPropertiesAsync() } } - string folderPath = (Item as ShortcutItem)?.TargetPath ?? Item.ItemPath; - BaseStorageFolder storageFolder = await AppInstance.ShellViewModel.GetFolderFromPathAsync(folderPath); - if (storageFolder is not null) { ViewModel.ItemCreatedTimestampReal = storageFolder.DateCreated; diff --git a/src/Files.App/ViewModels/ShellViewModel.cs b/src/Files.App/ViewModels/ShellViewModel.cs index 3538ce53f269..1fb7eac3e899 100644 --- a/src/Files.App/ViewModels/ShellViewModel.cs +++ b/src/Files.App/ViewModels/ShellViewModel.cs @@ -973,7 +973,7 @@ private async Task GetShieldIcon() return shieldIcon; } - private async Task LoadThumbnailAsync(ListedItem item, CancellationToken cancellationToken) + private async Task LoadThumbnailAsync(ListedItem item, CancellationToken cancellationToken) { var loadNonCachedThumbnail = false; var thumbnailSize = LayoutSizeKindHelper.GetIconSize(folderSettings.LayoutMode); @@ -1086,6 +1086,42 @@ await dispatcherQueue.EnqueueOrInvokeAsync(async () => } }, cancellationToken); } + + return result is not null; + } + + private async Task LoadThumbnailAsync(ListedItem item, IStorageItem matchingStorageItem, CancellationToken cancellationToken) + { + var thumbnailSize = LayoutSizeKindHelper.GetIconSize(folderSettings.LayoutMode); + // SingleItem returns image thumbnails in the correct aspect ratio for the grid layouts + // ListView is used for the details and columns layout + // We use ReturnOnlyIfCached because otherwise folders thumbnails have a black background, this has the downside the folder previews don't work + var iconOptions = matchingStorageItem switch + { + BaseStorageFolder => IconOptions.SingleItem | IconOptions.ReturnOnlyIfCached, + BaseStorageFile when thumbnailSize < 96 => IconOptions.ListView | IconOptions.ResizeThumbnail, + _ => IconOptions.SingleItem | IconOptions.ResizeThumbnail, + }; + + var result = await FileThumbnailHelper.GetIconAsync( + matchingStorageItem, + thumbnailSize, + iconOptions); + + if (result is not null) + { + await dispatcherQueue.EnqueueOrInvokeAsync(async () => + { + // Assign FileImage property + var image = await result.ToBitmapAsync(); + if (image is not null) + item.FileImage = image; + }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Normal); + + return true; + } + + return false; } private static void SetFileTag(ListedItem item) @@ -1128,7 +1164,7 @@ public async Task LoadExtendedItemPropertiesAsync(ListedItem item) } cts.Token.ThrowIfCancellationRequested(); - await LoadThumbnailAsync(item, cts.Token); + var wasThumbnailLoaded = await LoadThumbnailAsync(item, cts.Token); cts.Token.ThrowIfCancellationRequested(); if (item.IsLibrary || item.PrimaryItemAttribute == StorageItemTypes.File || item.IsArchive) @@ -1180,6 +1216,10 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + // For MTP devices load thumbnail using Storage API (#15084) + if (!wasThumbnailLoaded) + await LoadThumbnailAsync(item, matchingStorageFile, cts.Token); + SetFileTag(item); wasSyncStatusLoaded = true; } @@ -1250,6 +1290,10 @@ await dispatcherQueue.EnqueueOrInvokeAsync(() => }, Microsoft.UI.Dispatching.DispatcherQueuePriority.Low); + // For MTP devices load thumbnail using Storage API (#15084) + if (!wasThumbnailLoaded) + await LoadThumbnailAsync(item, matchingStorageFolder, cts.Token); + SetFileTag(item); wasSyncStatusLoaded = true; } diff --git a/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs index 28f3a26cefd6..e42111fefa22 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/BasePreviewModel.cs @@ -86,6 +86,7 @@ public async virtual Task> LoadPreviewAndDetailsAsync() { var result = await FileThumbnailHelper.GetIconAsync( Item.ItemPath, + Item.ItemFile, Constants.ShellIconSizes.Jumbo, false, IconOptions.None); diff --git a/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs index f4a2d746a3d1..20be89f82dae 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/FolderPreviewViewModel.cs @@ -29,6 +29,7 @@ private async Task LoadPreviewAndDetailsAsync() var result = await FileThumbnailHelper.GetIconAsync( Item.ItemPath, + Folder, Constants.ShellIconSizes.Jumbo, true, IconOptions.None);