From ddc74fe3746858b258f00e01dc09d51c9a762d3e Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Wed, 14 Jun 2023 12:04:14 +0200 Subject: [PATCH 1/5] support for archives in peek --- Directory.Packages.props | 1 + NOTICE.md | 1 + .../Converters/SizeToStringConverter.cs | 27 +++ .../peek/Peek.Common/Helpers/SizeHelper.cs | 21 ++ .../Controls/ArchiveControl.xaml | 136 +++++++++++ .../Controls/ArchiveControl.xaml.cs | 81 +++++++ .../peek/Peek.FilePreviewer/FilePreview.xaml | 11 +- .../Peek.FilePreviewer/FilePreview.xaml.cs | 7 +- .../Peek.FilePreviewer.csproj | 8 + .../Previewers/Archives/ArchivePreviewer.cs | 216 ++++++++++++++++++ .../Previewers/Archives/Helpers/IconCache.cs | 85 +++++++ .../Previewers/Archives/Models/ArchiveItem.cs | 38 +++ .../Models/ArchiveItemTemplateSelector.cs | 27 +++ .../Archives/Models/ArchiveItemType.cs | 12 + .../Previewers/Helpers/BitmapHelper.cs | 30 ++- .../Interfaces/IArchivePreviewer.cs | 21 ++ .../MediaPreviewer/Helpers/NativeMethods.cs | 13 ++ .../Previewers/PreviewerFactory.cs | 5 + .../peek/Peek.UI/Strings/en-us/Resources.resw | 12 + 19 files changed, 749 insertions(+), 3 deletions(-) create mode 100644 src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs create mode 100644 src/modules/peek/Peek.Common/Helpers/SizeHelper.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml create mode 100644 src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Helpers/IconCache.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemTemplateSelector.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemType.cs create mode 100644 src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IArchivePreviewer.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index ba9fd599160a..83bddb483595 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -45,6 +45,7 @@ + diff --git a/NOTICE.md b/NOTICE.md index fa87dd90ec0c..7ee76b5aa347 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -318,6 +318,7 @@ SOFTWARE. - NLog.Extensions.Logging 5.0.4 - NLog.Schema 5.0.4 - ScipBe.Common.Office.OneNote 3.0.1 +- SharpCompress 0.33.0 - StreamJsonRpc 2.14.24 - StyleCop.Analyzers 1.2.0-beta.435 - System.CommandLine 2.0.0-beta4.22272.1 diff --git a/src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs b/src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs new file mode 100644 index 000000000000..1ef04571051f --- /dev/null +++ b/src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.UI.Xaml.Data; +using Peek.Common.Helpers; + +namespace Peek.Common.Converters +{ + public class SizeToStringConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, string language) + { + if (value is long size) + { + return SizeHelper.GetHumanSize(size); + } + else + { + return value; + } + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException(); + } +} diff --git a/src/modules/peek/Peek.Common/Helpers/SizeHelper.cs b/src/modules/peek/Peek.Common/Helpers/SizeHelper.cs new file mode 100644 index 000000000000..369198fb5e20 --- /dev/null +++ b/src/modules/peek/Peek.Common/Helpers/SizeHelper.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Peek.Common.Helpers +{ + public static class SizeHelper + { + public static string GetHumanSize(long size) + { + return size switch + { + < 1024 => $"{size} bytes", + < 1024 * 1024 => $"{size / 1024.0:0.00} KB", + < 1024 * 1024 * 1024 => $"{size / 1024.0 / 1024.0:0.00} MB", + < 1024L * 1024 * 1024 * 1024 => $"{size / 1024.0 / 1024.0 / 1024.0:0.00} GB", + _ => $"{size / 1024.0 / 1024.0 / 1024.0 / 1024.0:0.00} TB", + }; + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml new file mode 100644 index 000000000000..ae5d485ebd81 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml.cs new file mode 100644 index 000000000000..591f9af7a477 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Peek.FilePreviewer.Previewers; +using Peek.FilePreviewer.Previewers.Archives; +using Peek.FilePreviewer.Previewers.Archives.Models; + +namespace Peek.FilePreviewer.Controls +{ + public sealed partial class ArchiveControl : UserControl + { + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( + nameof(Source), + typeof(ObservableCollection), + typeof(ArchivePreviewer), + new PropertyMetadata(null)); + + public static readonly DependencyProperty LoadingStateProperty = DependencyProperty.Register( + nameof(LoadingState), + typeof(PreviewState), + typeof(ArchivePreviewer), + new PropertyMetadata(PreviewState.Uninitialized)); + + public static readonly DependencyProperty DirectoryCountProperty = DependencyProperty.Register( + nameof(DirectoryCount), + typeof(string), + typeof(ArchivePreviewer), + new PropertyMetadata(null)); + + public static readonly DependencyProperty FileCountProperty = DependencyProperty.Register( + nameof(FileCount), + typeof(string), + typeof(ArchivePreviewer), + new PropertyMetadata(null)); + + public static readonly DependencyProperty SizeProperty = DependencyProperty.Register( + nameof(Size), + typeof(string), + typeof(ArchivePreviewer), + new PropertyMetadata(null)); + + public ObservableCollection? Source + { + get { return (ObservableCollection)GetValue(SourceProperty); } + set { SetValue(SourceProperty, value); } + } + + public PreviewState? LoadingState + { + get { return (PreviewState)GetValue(LoadingStateProperty); } + set { SetValue(LoadingStateProperty, value); } + } + + public string? DirectoryCount + { + get { return (string)GetValue(DirectoryCountProperty); } + set { SetValue(DirectoryCountProperty, value); } + } + + public string? FileCount + { + get { return (string)GetValue(FileCountProperty); } + set { SetValue(FileCountProperty, value); } + } + + public string? Size + { + get { return (string)GetValue(SizeProperty); } + set { SetValue(SizeProperty, value); } + } + + public ArchiveControl() + { + this.InitializeComponent(); + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml index f5035fab7f54..9d1539cdd944 100644 --- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml +++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml @@ -1,4 +1,4 @@ - + + + Previewer as IBrowserPreviewer; + public IArchivePreviewer? ArchivePreviewer => Previewer as IArchivePreviewer; + public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer; public IFileSystemItem Item @@ -145,6 +148,7 @@ private async Task OnItemPropertyChanged() ImagePreview.Visibility = Visibility.Collapsed; VideoPreview.Visibility = Visibility.Collapsed; BrowserPreview.Visibility = Visibility.Collapsed; + ArchivePreview.Visibility = Visibility.Collapsed; UnsupportedFilePreview.Visibility = Visibility.Collapsed; return; } @@ -207,6 +211,7 @@ partial void OnPreviewerChanging(IPreviewer? value) VideoPreview.Source = null; ImagePreview.Source = null; + ArchivePreview.Source = null; BrowserPreview.Source = null; if (Previewer != null) diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj index 4b351beb7b50..aee052e58dd6 100644 --- a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj +++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj @@ -14,6 +14,7 @@ + @@ -25,6 +26,7 @@ + @@ -35,6 +37,12 @@ + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs new file mode 100644 index 000000000000..4e4d6bc172f3 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Dispatching; +using Peek.Common.Extensions; +using Peek.Common.Helpers; +using Peek.Common.Models; +using Peek.FilePreviewer.Models; +using Peek.FilePreviewer.Previewers.Archives.Helpers; +using Peek.FilePreviewer.Previewers.Archives.Models; +using Peek.FilePreviewer.Previewers.Interfaces; +using SharpCompress.Archives; +using SharpCompress.Common; +using SharpCompress.Readers; +using Windows.ApplicationModel.Resources; + +namespace Peek.FilePreviewer.Previewers.Archives +{ + public partial class ArchivePreviewer : ObservableObject, IArchivePreviewer + { + private readonly IconCache _iconCache = new(); + private int _directoryCount; + private int _fileCount; + private long _size; + private long _extractedSize; + + [ObservableProperty] + private PreviewState state; + + [ObservableProperty] + private string? _directoryCountText; + + [ObservableProperty] + private string? _fileCountText; + + [ObservableProperty] + private string? _sizeText; + + private IFileSystemItem Item { get; } + + private DispatcherQueue Dispatcher { get; } + + public ObservableCollection Tree { get; } + + public ArchivePreviewer(IFileSystemItem file) + { + Item = file; + Dispatcher = DispatcherQueue.GetForCurrentThread(); + Tree = new ObservableCollection(); + } + + public async Task CopyAsync() + { + await Dispatcher.RunOnUiThread(async () => + { + var storageItem = await Item.GetStorageItemAsync(); + ClipboardHelper.SaveToClipboard(storageItem); + }); + } + + public Task GetPreviewSizeAsync(CancellationToken cancellationToken) + { + return Task.FromResult(new PreviewSize { MonitorSize = null }); + } + + public async Task LoadPreviewAsync(CancellationToken cancellationToken) + { + State = PreviewState.Loading; + using var stream = File.OpenRead(Item.Path); + + if (Item.Path.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || Item.Path.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) + { + using var archive = ArchiveFactory.Open(stream); + _extractedSize = archive.TotalUncompressSize; + stream.Seek(0, SeekOrigin.Begin); + + using var reader = ReaderFactory.Open(stream); + while (reader.MoveToNextEntry()) + { + cancellationToken.ThrowIfCancellationRequested(); + await AddEntryAsync(reader.Entry, cancellationToken); + } + } + else + { + using var archive = ArchiveFactory.Open(stream); + _extractedSize = archive.TotalUncompressSize; + + foreach (var entry in archive.Entries) + { + cancellationToken.ThrowIfCancellationRequested(); + await AddEntryAsync(entry, cancellationToken); + } + } + + _size = new FileInfo(Item.Path).Length; // archive.TotalSize isn't accurate + DirectoryCountText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_Directory_Count"), _directoryCount); + FileCountText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_File_Count"), _fileCount); + SizeText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_Size"), SizeHelper.GetHumanSize(_size), SizeHelper.GetHumanSize(_extractedSize)); + + State = PreviewState.Loaded; + } + + public static bool IsFileTypeSupported(string fileExt) + { + return _supportedFileTypes.Contains(fileExt); + } + + public void Dispose() + { + GC.SuppressFinalize(this); + } + + private async Task AddEntryAsync(IEntry entry, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(entry, nameof(entry)); + + var levels = entry!.Key + .Split('/', '\\') + .Where(l => !string.IsNullOrWhiteSpace(l)) + .ToArray(); + + ArchiveItem? parent = null; + for (var i = 0; i < levels.Length; i++) + { + var type = (!entry.IsDirectory && i == levels.Length - 1) ? ArchiveItemType.File : ArchiveItemType.Directory; + + var icon = type == ArchiveItemType.Directory + ? await _iconCache.GetDirectoryIconAsync(cancellationToken) + : await _iconCache.GetFileExtIconAsync(entry.Key, cancellationToken); + + var item = new ArchiveItem(levels[i], type, icon); + + if (type == ArchiveItemType.Directory) + { + item.IsExpanded = parent == null; // Only the root level is expanded + } + else if (type == ArchiveItemType.File) + { + item.Size = entry.Size; + } + + if (parent == null) + { + var existing = Tree.FirstOrDefault(e => e.Name == item.Name); + if (existing == null) + { + var index = GetIndex(Tree, item); + Tree.Insert(index, item); + CountItem(item); + } + + parent = existing ?? Tree.First(e => e.Name == item.Name); + } + else + { + var existing = parent.Children.FirstOrDefault(e => e.Name == item.Name); + if (existing == null) + { + var index = GetIndex(parent.Children, item); + parent.Children.Insert(index, item); + CountItem(item); + } + + parent = existing ?? parent.Children.First(e => e.Name == item.Name); + } + } + } + + private int GetIndex(ObservableCollection collection, ArchiveItem item) + { + for (var i = 0; i < collection.Count; i++) + { + if (item.Type == collection[i].Type && string.Compare(collection[i].Name, item.Name, StringComparison.OrdinalIgnoreCase) > 0) + { + return i; + } + } + + return item.Type switch + { + ArchiveItemType.Directory => collection.Count(e => e.Type == ArchiveItemType.Directory), + ArchiveItemType.File => collection.Count, + _ => 0, + }; + } + + private void CountItem(ArchiveItem item) + { + if (item.Type == ArchiveItemType.Directory) + { + _directoryCount++; + } + else if (item.Type == ArchiveItemType.File) + { + _fileCount++; + } + } + + private static readonly HashSet _supportedFileTypes = new() + { + ".zip", ".rar", ".7z", ".tar", ".nupkg", ".jar", ".gz", ".tar", ".tar.gz", ".tgz", + }; + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Helpers/IconCache.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Helpers/IconCache.cs new file mode 100644 index 000000000000..d9e3c009c98f --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Helpers/IconCache.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.UI.Xaml.Media.Imaging; +using Peek.Common; +using Peek.Common.Helpers; +using Peek.Common.Models; +using Peek.FilePreviewer.Previewers.Helpers; + +namespace Peek.FilePreviewer.Previewers.Archives.Helpers +{ + public class IconCache + { + private readonly Dictionary _cache = new(); + + private BitmapSource? _directoryIconCache; + + public async Task GetFileExtIconAsync(string fileName, CancellationToken cancellationToken) + { + var extension = Path.GetExtension(fileName); + + if (_cache.TryGetValue(extension, out var cachedIcon)) + { + return cachedIcon; + } + + try + { + var shFileInfo = default(SHFILEINFO); + if (NativeMethods.SHGetFileInfo(fileName, NativeMethods.FILE_ATTRIBUTE_NORMAL, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), NativeMethods.SHGFI_ICON | NativeMethods.SHGFI_SMALLICON | NativeMethods.SHGFI_USEFILEATTRIBUTES) != IntPtr.Zero) + { + var imageSource = await BitmapHelper.GetBitmapFromHIconAsync(shFileInfo.HIcon, cancellationToken); + _cache.Add(extension, imageSource); + return imageSource; + } + else + { + Logger.LogError($"Icon extraction for extension {extension} failed with error {Marshal.GetLastWin32Error()}"); + } + } + catch (Exception ex) + { + Logger.LogError($"Icon extraction for extension {extension} failed", ex); + } + + return null; + } + + public async Task GetDirectoryIconAsync(CancellationToken cancellationToken) + { + if (_directoryIconCache != null) + { + return _directoryIconCache; + } + + try + { + var shinfo = default(SHFILEINFO); + if (NativeMethods.SHGetFileInfo("directory", NativeMethods.FILE_ATTRIBUTE_DIRECTORY, ref shinfo, (uint)Marshal.SizeOf(shinfo), NativeMethods.SHGFI_ICON | NativeMethods.SHGFI_SMALLICON | NativeMethods.SHGFI_USEFILEATTRIBUTES) != IntPtr.Zero) + { + var imageSource = await BitmapHelper.GetBitmapFromHIconAsync(shinfo.HIcon, cancellationToken); + _directoryIconCache = imageSource; + return imageSource; + } + else + { + Logger.LogError($"Icon extraction for directory failed with error {Marshal.GetLastWin32Error()}"); + } + } + catch (Exception ex) + { + Logger.LogError($"Icon extraction for directory failed", ex); + } + + return null; + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs new file mode 100644 index 000000000000..f661d6beb3f6 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Xaml.Media; + +namespace Peek.FilePreviewer.Previewers.Archives.Models +{ + public partial class ArchiveItem : ObservableObject + { + [ObservableProperty] + private string _name; + + [ObservableProperty] + private ArchiveItemType _type; + + [ObservableProperty] + private ImageSource? _icon; + + [ObservableProperty] + private long _size; + + [ObservableProperty] + private bool _isExpanded; + + public ObservableCollection Children { get; } + + public ArchiveItem(string name, ArchiveItemType type, ImageSource? icon) + { + Name = name; + Type = type; + Icon = icon; + Children = new ObservableCollection(); + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemTemplateSelector.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemTemplateSelector.cs new file mode 100644 index 000000000000..38bd9766c047 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemTemplateSelector.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Peek.FilePreviewer.Previewers.Archives.Models +{ + public class ArchiveItemTemplateSelector : DataTemplateSelector + { + public DataTemplate? DirectoryTemplate { get; set; } + + public DataTemplate? FileTemplate { get; set; } + + protected override DataTemplate? SelectTemplateCore(object item) + { + if (item is ArchiveItem archiveItem) + { + return archiveItem.Type == ArchiveItemType.Directory ? DirectoryTemplate : FileTemplate; + } + + throw new ArgumentException("Item must be an ArchiveItem", nameof(item)); + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemType.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemType.cs new file mode 100644 index 000000000000..5abb4eb81b85 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItemType.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Peek.FilePreviewer.Previewers.Archives.Models +{ + public enum ArchiveItemType + { + Directory = 0, + File = 1, + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs index 37ec5dd16fdf..476c2d9bf755 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Helpers/BitmapHelper.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Drawing; using System.Drawing.Imaging; using System.IO; using System.Threading; @@ -18,7 +19,7 @@ public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, { try { - var bitmap = System.Drawing.Image.FromHbitmap(hbitmap); + var bitmap = Image.FromHbitmap(hbitmap); if (isSupportingTransparency) { bitmap.MakeTransparent(); @@ -44,5 +45,32 @@ public static async Task GetBitmapFromHBitmapAsync(IntPtr hbitmap, NativeMethods.DeleteObject(hbitmap); } } + + public static async Task GetBitmapFromHIconAsync(IntPtr hicon, CancellationToken cancellationToken) + { + try + { + var icon = (Icon)Icon.FromHandle(hicon).Clone(); + var bitmap = icon.ToBitmap(); + + var bitmapImage = new BitmapImage(); + + using (var stream = new MemoryStream()) + { + bitmap.Save(stream, ImageFormat.Png); + stream.Position = 0; + + cancellationToken.ThrowIfCancellationRequested(); + await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream()); + } + + return bitmapImage; + } + finally + { + // Delete HIcon to avoid memory leaks + _ = NativeMethods.DestroyIcon(hicon); + } + } } } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IArchivePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IArchivePreviewer.cs new file mode 100644 index 000000000000..0c813f60d3d8 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IArchivePreviewer.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.ObjectModel; +using Peek.FilePreviewer.Previewers.Archives.Models; + +namespace Peek.FilePreviewer.Previewers.Interfaces +{ + public interface IArchivePreviewer : IPreviewer, IDisposable + { + ObservableCollection Tree { get; } + + string? DirectoryCountText { get; } + + string? FileCountText { get; } + + string? SizeText { get; } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/NativeMethods.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/NativeMethods.cs index fcba6abe923d..268f910dcfa9 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/NativeMethods.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Helpers/NativeMethods.cs @@ -10,6 +10,13 @@ namespace Peek.Common { public static class NativeMethods { + internal const uint SHGFI_ICON = 0x000000100; + internal const uint SHGFI_LINKOVERLAY = 0x000008000; + internal const uint SHGFI_SMALLICON = 0x000000001; + internal const uint SHGFI_USEFILEATTRIBUTES = 0x000000010; + internal const uint FILE_ATTRIBUTE_NORMAL = 0x00000080; + internal const uint FILE_ATTRIBUTE_DIRECTORY = 0x00000010; + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName( [MarshalAs(UnmanagedType.LPWStr)] string path, @@ -17,8 +24,14 @@ internal static extern int SHCreateItemFromParsingName( ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem); + [DllImport("User32.dll", SetLastError = true)] + internal static extern int DestroyIcon(IntPtr hIcon); + [DllImport("gdi32.dll")] [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool DeleteObject(IntPtr hObject); + + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + internal static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbFileInfo, uint uFlags); } } diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs index f03a8435bebd..3ac9ceeceec7 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs @@ -4,6 +4,7 @@ using Microsoft.PowerToys.Telemetry; using Peek.Common.Models; +using Peek.FilePreviewer.Previewers.Archives; using Peek.UI.Telemetry.Events; namespace Peek.FilePreviewer.Previewers @@ -24,6 +25,10 @@ public IPreviewer Create(IFileSystemItem file) { return new WebBrowserPreviewer(file); } + else if (ArchivePreviewer.IsFileTypeSupported(file.Extension)) + { + return new ArchivePreviewer(file); + } // Other previewer types check their supported file types here return CreateDefaultPreviewer(file); diff --git a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw index b940d7968f99..0d032785b17f 100644 --- a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw +++ b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw @@ -221,4 +221,16 @@ An error occurred while previewing this file. Failed fallback preview text. + + {0} directories + {0} is the number of directories in the archive + + + {0} files + {0} is the number of files in the archive + + + {0} (extracted {1}) + {0} is the size of the archive, {1} is the extracted size + \ No newline at end of file From 1bc7e4dd2cd655705fef60e2fa8466dabfcc72e3 Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Wed, 14 Jun 2023 12:21:35 +0200 Subject: [PATCH 2/5] fix spellcheck --- .github/actions/spell-check/expect.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 24ff597723df..a3d30fb97a7e 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1087,6 +1087,7 @@ LIGHTORANGE LIGHTTURQUOISE lindex linkedin +LINKOVERLAY linq LINQTo listview @@ -1415,6 +1416,7 @@ nullonfailure numberbox NUMLOCK numpad +nupkg nwc Objbase OBJID @@ -1613,6 +1615,7 @@ psapi pscid PSECURITY psfgao +psfi Psr psrm psrree @@ -1851,6 +1854,7 @@ shellscalingapi SHFILEINFO SHGDNF SHGFI +shinfo Shl shldisp shlobj @@ -2056,6 +2060,7 @@ TEXCOORD textblock TEXTEXTRACTOR TEXTINCLUDE +tgz themeresources THH THICKFRAME @@ -2123,6 +2128,7 @@ ULONGLONG unapply unassign uncompilable +Uncompress UNCPRIORITY UNDNAME UNICODETEXT From ea63f9355b036ba1cfda114e0c4a1a715b4c3ce4 Mon Sep 17 00:00:00 2001 From: Davide Giacometti Date: Thu, 15 Jun 2023 11:08:51 +0200 Subject: [PATCH 3/5] horizontal scrolling --- .../Controls/ArchiveControl.xaml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml index ae5d485ebd81..f79c24ad1377 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml @@ -75,14 +75,17 @@ - + HorizontalScrollBarVisibility="Auto"> + + Date: Mon, 19 Jun 2023 11:10:04 +0200 Subject: [PATCH 4/5] fix height --- .../peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml index f79c24ad1377..481ce2d073ed 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml @@ -113,7 +113,6 @@ TextWrapping="Wrap" /> Date: Tue, 27 Jun 2023 16:35:32 +0200 Subject: [PATCH 5/5] removed redundant helper --- ...Converter.cs => BytesToStringConverter.cs} | 6 +++--- .../peek/Peek.Common/Helpers/SizeHelper.cs | 21 ------------------- .../Controls/ArchiveControl.xaml | 4 ++-- .../Previewers/Archives/ArchivePreviewer.cs | 14 ++++++------- .../Previewers/Archives/Models/ArchiveItem.cs | 2 +- 5 files changed, 13 insertions(+), 34 deletions(-) rename src/modules/peek/Peek.Common/Converters/{SizeToStringConverter.cs => BytesToStringConverter.cs} (79%) delete mode 100644 src/modules/peek/Peek.Common/Helpers/SizeHelper.cs diff --git a/src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs b/src/modules/peek/Peek.Common/Converters/BytesToStringConverter.cs similarity index 79% rename from src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs rename to src/modules/peek/Peek.Common/Converters/BytesToStringConverter.cs index 1ef04571051f..fa40547daf5d 100644 --- a/src/modules/peek/Peek.Common/Converters/SizeToStringConverter.cs +++ b/src/modules/peek/Peek.Common/Converters/BytesToStringConverter.cs @@ -8,13 +8,13 @@ namespace Peek.Common.Converters { - public class SizeToStringConverter : IValueConverter + public class BytesToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, string language) { - if (value is long size) + if (value is ulong size) { - return SizeHelper.GetHumanSize(size); + return ReadableStringHelper.BytesToReadableString(size); } else { diff --git a/src/modules/peek/Peek.Common/Helpers/SizeHelper.cs b/src/modules/peek/Peek.Common/Helpers/SizeHelper.cs deleted file mode 100644 index 369198fb5e20..000000000000 --- a/src/modules/peek/Peek.Common/Helpers/SizeHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -namespace Peek.Common.Helpers -{ - public static class SizeHelper - { - public static string GetHumanSize(long size) - { - return size switch - { - < 1024 => $"{size} bytes", - < 1024 * 1024 => $"{size / 1024.0:0.00} KB", - < 1024 * 1024 * 1024 => $"{size / 1024.0 / 1024.0:0.00} MB", - < 1024L * 1024 * 1024 * 1024 => $"{size / 1024.0 / 1024.0 / 1024.0:0.00} GB", - _ => $"{size / 1024.0 / 1024.0 / 1024.0 / 1024.0:0.00} TB", - }; - } - } -} diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml index 481ce2d073ed..346d0e82c851 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml +++ b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml @@ -58,7 +58,7 @@ + Text="{Binding Size, Converter={StaticResource BytesToStringConverter}}" /> @@ -68,7 +68,7 @@ DirectoryTemplate="{StaticResource DirectoryTemplate}" FileTemplate="{StaticResource FileTemplate}" /> - + diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs index 4e4d6bc172f3..ccf8987b8205 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/ArchivePreviewer.cs @@ -32,8 +32,8 @@ public partial class ArchivePreviewer : ObservableObject, IArchivePreviewer private readonly IconCache _iconCache = new(); private int _directoryCount; private int _fileCount; - private long _size; - private long _extractedSize; + private ulong _size; + private ulong _extractedSize; [ObservableProperty] private PreviewState state; @@ -82,7 +82,7 @@ public async Task LoadPreviewAsync(CancellationToken cancellationToken) if (Item.Path.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) || Item.Path.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) { using var archive = ArchiveFactory.Open(stream); - _extractedSize = archive.TotalUncompressSize; + _extractedSize = (ulong)archive.TotalUncompressSize; stream.Seek(0, SeekOrigin.Begin); using var reader = ReaderFactory.Open(stream); @@ -95,7 +95,7 @@ public async Task LoadPreviewAsync(CancellationToken cancellationToken) else { using var archive = ArchiveFactory.Open(stream); - _extractedSize = archive.TotalUncompressSize; + _extractedSize = (ulong)archive.TotalUncompressSize; foreach (var entry in archive.Entries) { @@ -104,10 +104,10 @@ public async Task LoadPreviewAsync(CancellationToken cancellationToken) } } - _size = new FileInfo(Item.Path).Length; // archive.TotalSize isn't accurate + _size = (ulong)new FileInfo(Item.Path).Length; // archive.TotalSize isn't accurate DirectoryCountText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_Directory_Count"), _directoryCount); FileCountText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_File_Count"), _fileCount); - SizeText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_Size"), SizeHelper.GetHumanSize(_size), SizeHelper.GetHumanSize(_extractedSize)); + SizeText = string.Format(CultureInfo.CurrentCulture, ResourceLoader.GetForViewIndependentUse().GetString("Archive_Size"), ReadableStringHelper.BytesToReadableString(_size), ReadableStringHelper.BytesToReadableString(_extractedSize)); State = PreviewState.Loaded; } @@ -148,7 +148,7 @@ private async Task AddEntryAsync(IEntry entry, CancellationToken cancellationTok } else if (type == ArchiveItemType.File) { - item.Size = entry.Size; + item.Size = (ulong)entry.Size; } if (parent == null) diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs index f661d6beb3f6..2c34456da772 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Archives/Models/ArchiveItem.cs @@ -20,7 +20,7 @@ public partial class ArchiveItem : ObservableObject private ImageSource? _icon; [ObservableProperty] - private long _size; + private ulong _size; [ObservableProperty] private bool _isExpanded;