diff --git a/Directory.Build.props b/Directory.Build.props
index e32e82a180..da2760b068 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -85,7 +85,7 @@
false
- 202310210737
+ 202406130737
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderInformationalPreviewControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderInformationalPreviewControl.xaml.cs
new file mode 100644
index 0000000000..9ae7a610ae
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderInformationalPreviewControl.xaml.cs
@@ -0,0 +1,41 @@
+// 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 Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Peek.Common.Helpers;
+using Peek.FilePreviewer.Models;
+
+namespace Peek.FilePreviewer.Controls;
+
+public sealed partial class SpecialFolderInformationalPreviewControl : UserControl
+{
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
+ nameof(Source),
+ typeof(SpecialFolderPreviewData),
+ typeof(SpecialFolderInformationalPreviewControl),
+ new PropertyMetadata(null));
+
+ public SpecialFolderPreviewData? Source
+ {
+ get { return (SpecialFolderPreviewData)GetValue(SourceProperty); }
+ set { SetValue(SourceProperty, value); }
+ }
+
+ public SpecialFolderInformationalPreviewControl()
+ {
+ InitializeComponent();
+ }
+
+ public string FormatFileType(string? fileType) => FormatField("UnsupportedFile_FileType", fileType);
+
+ public string FormatFileSize(string? fileSize) => FormatField("UnsupportedFile_FileSize", fileSize);
+
+ public string FormatFileDateModified(string? fileDateModified) => FormatField("UnsupportedFile_DateModified", fileDateModified);
+
+ private static string FormatField(string resourceId, string? fieldValue)
+ {
+ return string.IsNullOrWhiteSpace(fieldValue) ? string.Empty : ReadableStringHelper.FormatResourceString(resourceId, fieldValue);
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderPreview.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderPreview.xaml
new file mode 100644
index 0000000000..ec08d0396d
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderPreview.xaml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderPreview.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderPreview.xaml.cs
new file mode 100644
index 0000000000..b1a468c3fa
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Controls/SpecialFolderPreview/SpecialFolderPreview.xaml.cs
@@ -0,0 +1,47 @@
+// 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 CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Peek.Common.Converters;
+using Peek.FilePreviewer.Models;
+using Peek.FilePreviewer.Previewers;
+
+namespace Peek.FilePreviewer.Controls;
+
+[INotifyPropertyChanged]
+public sealed partial class SpecialFolderPreview : UserControl
+{
+ public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
+ nameof(Source),
+ typeof(SpecialFolderPreviewData),
+ typeof(SpecialFolderPreview),
+ new PropertyMetadata(null));
+
+ public static readonly DependencyProperty LoadingStateProperty = DependencyProperty.Register(
+ nameof(LoadingState),
+ typeof(PreviewState),
+ typeof(SpecialFolderPreview),
+ new PropertyMetadata(PreviewState.Uninitialized));
+
+ public SpecialFolderPreviewData? Source
+ {
+ get { return (SpecialFolderPreviewData)GetValue(SourceProperty); }
+ set { SetValue(SourceProperty, value); }
+ }
+
+ public PreviewState? LoadingState
+ {
+ get { return (PreviewState)GetValue(LoadingStateProperty); }
+ set { SetValue(LoadingStateProperty, value); }
+ }
+
+ public SpecialFolderPreview()
+ {
+ InitializeComponent();
+ }
+
+ public Visibility IsVisibleIfStatesMatch(PreviewState? a, PreviewState? b) => VisibilityConverter.Convert(a == b);
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
index 67ffe62d84..5d47fbe962 100644
--- a/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
+++ b/src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
@@ -83,6 +83,12 @@
Source="{x:Bind DrivePreviewer.Preview, Mode=OneWay}"
Visibility="{x:Bind IsPreviewVisible(DrivePreviewer, Previewer.State), Mode=OneWay}" />
+
+
Previewer as IDrivePreviewer;
+ public ISpecialFolderPreviewer? SpecialFolderPreviewer => Previewer as ISpecialFolderPreviewer;
+
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
public IFileSystemItem Item
diff --git a/src/modules/peek/Peek.FilePreviewer/Models/SpecialFolderPreviewData.cs b/src/modules/peek/Peek.FilePreviewer/Models/SpecialFolderPreviewData.cs
new file mode 100644
index 0000000000..c6549ba50a
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Models/SpecialFolderPreviewData.cs
@@ -0,0 +1,26 @@
+// 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 CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Xaml.Media;
+
+namespace Peek.FilePreviewer.Models;
+
+public partial class SpecialFolderPreviewData : ObservableObject
+{
+ [ObservableProperty]
+ private ImageSource? iconPreview;
+
+ [ObservableProperty]
+ private string? fileName;
+
+ [ObservableProperty]
+ private string? fileType;
+
+ [ObservableProperty]
+ private string? fileSize;
+
+ [ObservableProperty]
+ private string? dateModified;
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
index 8ffcb6cc8a..495a21289f 100644
--- a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
+++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj
@@ -21,6 +21,8 @@
+
+
@@ -42,6 +44,27 @@
+
+
+ 0
+ 1
+ 50a7e9b0-70ef-11d1-b75a-00a0c90564fe
+ 0
+ tlbimp
+ false
+ true
+
+
+ 1
+ 1
+ eab22ac0-30c1-11cf-a7eb-0000c05bae0b
+ 0
+ tlbimp
+ false
+ true
+
+
+
@@ -71,6 +94,9 @@
MSBuild:Compile
+
+ MSBuild:Compile
+
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/ISpecialFolderPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/ISpecialFolderPreviewer.cs
new file mode 100644
index 0000000000..a5d663a2e4
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/ISpecialFolderPreviewer.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.
+
+using Peek.FilePreviewer.Models;
+
+namespace Peek.FilePreviewer.Previewers;
+
+public interface ISpecialFolderPreviewer : IPreviewer
+{
+ public SpecialFolderPreviewData? Preview { get; }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs
index 158521b3ca..cce79a6235 100644
--- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs
@@ -53,6 +53,10 @@ namespace Peek.FilePreviewer.Previewers
{
return new DrivePreviewer(item);
}
+ else if (SpecialFolderPreviewer.IsItemSupported(item))
+ {
+ return new SpecialFolderPreviewer(item);
+ }
// Other previewer types check their supported file types here
return CreateDefaultPreviewer(item);
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/SpecialFolderPreviewer/Helpers/KnownSpecialFolders.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/SpecialFolderPreviewer/Helpers/KnownSpecialFolders.cs
new file mode 100644
index 0000000000..930cc5cc60
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/SpecialFolderPreviewer/Helpers/KnownSpecialFolders.cs
@@ -0,0 +1,58 @@
+// 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.Linq;
+using Peek.Common;
+using Peek.Common.Models;
+
+namespace Peek.FilePreviewer.Previewers;
+
+public enum KnownSpecialFolder
+{
+ None,
+ RecycleBin,
+}
+
+public static class KnownSpecialFolders
+{
+ private static readonly Lazy> FoldersByParsingNameDict = new(GetFoldersByParsingName);
+
+ public static IReadOnlyDictionary FoldersByParsingName => FoldersByParsingNameDict.Value;
+
+ private static Dictionary GetFoldersByParsingName()
+ {
+ var folders = new (KnownSpecialFolder Folder, string? ParsingName)[]
+ {
+ (KnownSpecialFolder.RecycleBin, GetParsingName("shell:RecycleBinFolder")),
+ };
+
+ return folders.Where(folder => !string.IsNullOrEmpty(folder.ParsingName))
+ .ToDictionary(folder => folder.ParsingName!, folder => folder.Folder);
+ }
+
+ private static string? GetParsingName(string shellName)
+ {
+ try
+ {
+ return CreateShellItemFromShellName(shellName)?.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_DESKTOPABSOLUTEPARSING);
+ }
+ catch (Exception)
+ {
+ return null;
+ }
+ }
+
+ private static IShellItem? CreateShellItemFromShellName(string shellName)
+ {
+ // Based on https://stackoverflow.com/a/42966899
+ const string ShellItem = "43826d1e-e718-42ee-bc55-a1e261c37bfe";
+
+ Guid shellItem2Guid = new(ShellItem);
+ int retCode = NativeMethods.SHCreateItemFromParsingName(shellName, IntPtr.Zero, ref shellItem2Guid, out IShellItem? nativeShellItem);
+
+ return retCode == 0 ? nativeShellItem : null;
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/SpecialFolderPreviewer/SpecialFolderPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/SpecialFolderPreviewer/SpecialFolderPreviewer.cs
new file mode 100644
index 0000000000..124071e54a
--- /dev/null
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/SpecialFolderPreviewer/SpecialFolderPreviewer.cs
@@ -0,0 +1,215 @@
+// 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.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;
+using Peek.Common.Models;
+using Peek.FilePreviewer.Models;
+using Peek.FilePreviewer.Previewers.Helpers;
+using Windows.Foundation;
+
+namespace Peek.FilePreviewer.Previewers;
+
+public partial class SpecialFolderPreviewer : ObservableObject, ISpecialFolderPreviewer, IDisposable
+{
+ private readonly DispatcherTimer _syncDetailsDispatcherTimer = new();
+ private ulong _folderSize;
+ private DateTime? _dateModified;
+
+ [ObservableProperty]
+ private SpecialFolderPreviewData preview = new();
+
+ [ObservableProperty]
+ private PreviewState state;
+
+ public SpecialFolderPreviewer(IFileSystemItem file)
+ {
+ _syncDetailsDispatcherTimer.Interval = TimeSpan.FromMilliseconds(500);
+ _syncDetailsDispatcherTimer.Tick += DetailsDispatcherTimer_Tick;
+
+ Item = file;
+ Preview.FileName = file.Name;
+ Dispatcher = DispatcherQueue.GetForCurrentThread();
+ }
+
+ public static bool IsItemSupported(IFileSystemItem item)
+ {
+ // Always allow know special folders.
+ bool isKnownSpecialFolder = KnownSpecialFolders.FoldersByParsingName.ContainsKey(item.ParsingName);
+
+ // Allow empty paths unless Unc; icons don't load correctly for Unc paths.
+ bool isEmptyNonUncPath = string.IsNullOrEmpty(item.Path) && !PathHelper.IsUncPath(item.ParsingName);
+
+ return isKnownSpecialFolder || isEmptyNonUncPath;
+ }
+
+ private IFileSystemItem Item { get; }
+
+ private DispatcherQueue Dispatcher { get; }
+
+ public void Dispose()
+ {
+ _syncDetailsDispatcherTimer.Tick -= DetailsDispatcherTimer_Tick;
+ GC.SuppressFinalize(this);
+ }
+
+ public Task GetPreviewSizeAsync(CancellationToken cancellationToken)
+ {
+ Size? size = new(680, 500);
+ var previewSize = new PreviewSize { MonitorSize = size, UseEffectivePixels = true };
+ return Task.FromResult(previewSize);
+ }
+
+ public async Task LoadPreviewAsync(CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ State = PreviewState.Loading;
+
+ var tasks = await Task.WhenAll(LoadIconPreviewAsync(cancellationToken), LoadDisplayInfoAsync(cancellationToken));
+
+ State = tasks.All(task => task) ? PreviewState.Loaded : PreviewState.Error;
+ }
+
+ public async Task CopyAsync()
+ {
+ await Dispatcher.RunOnUiThread(async () =>
+ {
+ var storageItem = await Item.GetStorageItemAsync();
+ ClipboardHelper.SaveToClipboard(storageItem);
+ });
+ }
+
+ public 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 IconHelper.GetIconAsync(Item.ParsingName, cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ isIconValid = iconBitmap != null;
+ Preview.IconPreview = iconBitmap ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg"));
+ });
+ });
+
+ return isIconValid && isTaskSuccessful;
+ }
+
+ public async Task LoadDisplayInfoAsync(CancellationToken cancellationToken)
+ {
+ bool isDisplayValid = false;
+
+ var isTaskSuccessful = await TaskExtension.RunSafe(async () =>
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var fileType = await Task.Run(Item.GetContentTypeAsync);
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ isDisplayValid = fileType != null;
+
+ await Dispatcher.RunOnUiThread(() =>
+ {
+ Preview.FileType = fileType;
+ return Task.CompletedTask;
+ });
+
+ RunUpdateDetailsWorkflow(cancellationToken);
+ });
+
+ return isDisplayValid && isTaskSuccessful;
+ }
+
+ private void RunUpdateDetailsWorkflow(CancellationToken cancellationToken)
+ {
+ Task.Run(
+ async () =>
+ {
+ try
+ {
+ await Dispatcher.RunOnUiThread(_syncDetailsDispatcherTimer.Start);
+ ComputeDetails(cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ }
+ catch (Exception ex)
+ {
+ Logger.LogError("Failed to update special folder details", ex);
+ }
+ finally
+ {
+ await Dispatcher.RunOnUiThread(_syncDetailsDispatcherTimer.Stop);
+ }
+
+ await Dispatcher.RunOnUiThread(SyncDetails);
+ },
+ cancellationToken);
+ }
+
+ private void ComputeDetails(CancellationToken cancellationToken)
+ {
+ _dateModified = Item.DateModified;
+
+ switch (KnownSpecialFolders.FoldersByParsingName.GetValueOrDefault(Item.ParsingName, KnownSpecialFolder.None))
+ {
+ case KnownSpecialFolder.None:
+ break;
+
+ case KnownSpecialFolder.RecycleBin:
+ ThreadHelper.RunOnSTAThread(() => { ComputeRecycleBinDetails(cancellationToken); });
+ cancellationToken.ThrowIfCancellationRequested();
+ break;
+ }
+ }
+
+ private void ComputeRecycleBinDetails(CancellationToken cancellationToken)
+ {
+ var shell = new Shell32.Shell();
+ var recycleBin = shell.NameSpace(10); // CSIDL_BITBUCKET
+
+ foreach (dynamic item in recycleBin.Items())
+ {
+ if (cancellationToken.IsCancellationRequested)
+ {
+ return;
+ }
+
+ _folderSize += Convert.ToUInt64(item.Size);
+ }
+ }
+
+ private void SyncDetails()
+ {
+ Preview.FileSize = _folderSize == 0 ? string.Empty : ReadableStringHelper.BytesToReadableString(_folderSize);
+ Preview.DateModified = _dateModified?.ToString(CultureInfo.CurrentCulture) ?? string.Empty;
+ }
+
+ private void DetailsDispatcherTimer_Tick(object? sender, object e)
+ {
+ SyncDetails();
+ }
+}
diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs
index 1b6b526bd0..185ca19ee9 100644
--- a/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs
+++ b/src/modules/peek/Peek.FilePreviewer/Previewers/UnsupportedFilePreviewer/UnsupportedFilePreviewer.cs
@@ -45,8 +45,6 @@ namespace Peek.FilePreviewer.Previewers
Dispatcher = DispatcherQueue.GetForCurrentThread();
}
- public bool IsPreviewLoaded => Preview.IconPreview != null;
-
private IFileSystemItem Item { get; }
private DispatcherQueue Dispatcher { get; }
diff --git a/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs b/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs
index 3d2fd2e57f..7e4eee002b 100644
--- a/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs
+++ b/src/modules/peek/Peek.UI/Extensions/IShellItemExtensions.cs
@@ -7,49 +7,41 @@ using System.IO;
using ManagedCommon;
using Peek.Common.Models;
-namespace Peek.UI.Extensions
+namespace Peek.UI.Extensions;
+
+public static class IShellItemExtensions
{
- public static class IShellItemExtensions
+ public static IFileSystemItem ToIFileSystemItem(this IShellItem shellItem)
{
- public static IFileSystemItem ToIFileSystemItem(this IShellItem shellItem)
- {
- string path = shellItem.GetPath();
- string name = shellItem.GetName();
+ string path = shellItem.GetPath();
+ string name = shellItem.GetName();
- return File.Exists(path) ? new FileItem(path, name) : new FolderItem(path, name);
+ return File.Exists(path) ? new FileItem(path, name) : new FolderItem(path, name, shellItem.GetParsingName());
+ }
+
+ private static string GetPath(this IShellItem shellItem) =>
+ shellItem.GetNameCore(Windows.Win32.UI.Shell.SIGDN.SIGDN_FILESYSPATH, logError: false);
+
+ private static string GetName(this IShellItem shellItem) =>
+ shellItem.GetNameCore(Windows.Win32.UI.Shell.SIGDN.SIGDN_NORMALDISPLAY, logError: true);
+
+ private static string GetParsingName(this IShellItem shellItem) =>
+ shellItem.GetNameCore(Windows.Win32.UI.Shell.SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, logError: true);
+
+ private static string GetNameCore(this IShellItem shellItem, Windows.Win32.UI.Shell.SIGDN displayNameType, bool logError)
+ {
+ try
+ {
+ return shellItem.GetDisplayName(displayNameType);
}
-
- private static string GetPath(this IShellItem shellItem)
+ catch (Exception ex)
{
- string path = string.Empty;
- try
+ if (logError)
{
- path = shellItem.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_FILESYSPATH);
- }
- catch (Exception ex)
- {
- // TODO: Handle cases that do not have a file system path like Recycle Bin.
- path = string.Empty;
- Logger.LogError("Getting path failed. " + ex.Message);
+ Logger.LogError($"Getting {Enum.GetName(displayNameType)} failed. {ex.Message}");
}
- return path;
- }
-
- private static string GetName(this IShellItem shellItem)
- {
- string name = string.Empty;
- try
- {
- name = shellItem.GetDisplayName(Windows.Win32.UI.Shell.SIGDN.SIGDN_NORMALDISPLAY);
- }
- catch (Exception ex)
- {
- name = string.Empty;
- Logger.LogError("Getting path failed. " + ex.Message);
- }
-
- return name;
+ return string.Empty;
}
}
}