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
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/BytesToStringConverter.cs b/src/modules/peek/Peek.Common/Converters/BytesToStringConverter.cs
new file mode 100644
index 000000000000..fa40547daf5d
--- /dev/null
+++ b/src/modules/peek/Peek.Common/Converters/BytesToStringConverter.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 BytesToStringConverter : IValueConverter
+ {
+ public object Convert(object value, Type targetType, object parameter, string language)
+ {
+ if (value is ulong size)
+ {
+ return ReadableStringHelper.BytesToReadableString(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.FilePreviewer/Controls/ArchiveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml
new file mode 100644
index 000000000000..346d0e82c851
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/ArchiveControl.xaml
@@ -0,0 +1,137 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..ccf8987b8205
--- /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 ulong _size;
+ private ulong _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 = (ulong)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 = (ulong)archive.TotalUncompressSize;
+
+ foreach (var entry in archive.Entries)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ await AddEntryAsync(entry, cancellationToken);
+ }
+ }
+
+ _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"), ReadableStringHelper.BytesToReadableString(_size), ReadableStringHelper.BytesToReadableString(_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 = (ulong)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..2c34456da772
--- /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 ulong _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