mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-06 19:26:39 +02:00
[Peek] Powertoys Peek MVP (#25922)
* Peek (#22498) * Add peek dll project * add spacebar preview and launch on hotkey press * add todo * add process handle to handle continuous press of hotkey * add tool to stop all powertoys processes * Add a blank Peek page and update nav menu * Add some initial content to Peek page including a toggle * refactor settings parsing * rename spacebar peek to peek viewer * rename script to stop powertoys processes * remove tool * Adding FileUtils for retrieving selected file in File Explorer * Remove unnecessary SndPeekSettings * Add shortcut setting * Set the shortcut to ctrl+space * Launching viewer with selected FE file * Add PeekUI WinUI3 project with interop events * Moving FileTypeUtils into PeekFileUtils project * execute winui3 app on hotkey * Fix paths with spaces * remove winui3 project * Resolve comment * add wpf app with toggle visibility on hotkey * fix visibility on startup * remove window properties and add todos * Fixed hidden extension and system file handling * wip * Add working WPF app with FileExplorer querying * remove c++ projects * Move native awaiter * Working Image control with image files * Resize and move window based on explorer monitor * Image render, window positioning and sizing clean up * add window management logic and selection logic * add extension methods to add circular iterating capability to linkedlistnode * Add OnArrowKeyPresshandler * Added titlebar with file name and scaling with titlebar height * fix flashing window on startup and process kept alive when powertoys exits * remove wait for debugger loop in ui * Add KeyIsDown method * Fix KeyDown issue with Key handled and check for repeat * Add thumbnail logic * Add all folder items if only one item is selected * File type helper * Using hresult * Add cancellation and rotation handling * Use extension instead of path * fIX CONFLICTS * Fixing some file type checks * Add new icon for Peek * Update page with the new Peek icon * Initialize IsEnabled and hook ActivationShortcut to dllmain * add icon to taskbar and titlebar * Add theme sensitive backgrounds * rename event handlers * add settings image * Move window data into obserable object * Refactor viewmodel, interop and helpers * Clean up * Add loading spinner * Add todos * Fix conflicts * Move native code into its own folder * Add peek to installer * Fix building peek and peekui projects * Replace UWP namespaces to WinAppSDK * Working WASDK placeholder project * Add exit when powertoys runner exit * Working winui3 with image display * Add WIC project with <TreatWarningAsErros> false for now * Fit content to window * Use Size from Windows.Foundation * Change order * Add some todos * Refactored native/interop code and added helpers to imagepreviewer * Rename projects * Move some code * Remove using Co-authored-by: Michael Salmon <miksalmon@users.noreply.github.com> Co-authored-by: Michael Salmon 🐟 <michaelpsalmon@outlook.com> Co-authored-by: Alireza Ebadi Ghajari <alirezae@microsoft.com> Co-authored-by: Jessie Su <Jessie.Su@microsoft.com> Co-authored-by: sujessie <102062556+sujessie@users.noreply.github.com> * Bump Microsoft.Windows.SDK.BuildTools version * [Peek] Plugin pattern to enable any file type previewing (#22475) * [Peek] Fetching image size through PropertyStore (#22530) * Fetching metadata from PropertySTore * Releasing objects to fix crash * Creating new PropertyHelper Co-authored-by: Daniel Chau <dancha@microsoft.com> * Juliata/filetypes (#22538) * Using the same list of file extensions as Lightbox's AppxManifest, and ensuring we convert file extension to lowercase * Add IsFileTypeSupported to IPreviewer * respond to PR comments * Add scale awareness to window centering (#22541) * [Peek] Fix installer builds, project configs and update assets (#22540) * Update installer * Fix installer errors * Fix peek vcxproj * Add package signing * Add peek to arm64 * Add back ARM64 toMeasureToolUI * Add versions to project * Update assets and icons * Add correct icon * [Peek] Enable PropertyStore for offline files (#22567) * Enabling PropertyStore for offline files Co-authored-by: Daniel Chau <dancha@microsoft.com> * [Peek] Adding unsupported file previewer (#22598) * Unsupported file previewer * Fix file display info * Fix property store calls * Update TODO * [Peek] Add WebView2 integration (#22506) * First commit with WIP logic to support WV2 in Peek module * Minor code cleanup and try/catch block * Added control to wrap WebView2 logic * Cleanup * Added logic to handle HTML previewing Properly update FilePreview according to file type * Code cleanup Updated comments * Updated comment * Removed comment * Code cleanup * Improved opening of web browser preview to avoid "blank" or "seeing previous page" issue Removed unused method Added xaml fallback to guarantee default/starting state * Removed folder * Updated factory logic to match master * address code review * addressed PR review * address PR review * Address PR review * address PR review * Address PR review * [Peek] Add basic file querying and navigation (#22589) * Refactor to facilitate file data initialization * Extract file-related code to new FileManager class * Add temp basic version * Clean + add todo for cancellations * Fix various nav-related issues * Temp - start moving iteration-related code to bg thread * Minor tweaks * Add FEHelper todo * Rename FileManager + various tweaks * Add basic throttling * Improve bg thread synchronization * Clean * Clean * Rename based on feedback * Rename FileQuery * Rename properties * Rename remaining fields * Add todos for nav success/failures Co-authored-by: Esteban Margaron <emargaron@microsoft.com> * [Peek] Add customized title bar (#22600) * Add basic button UI * Add function to get default app name and to open file in default app * Correct error output * Add filename to titlebar * Remove titlebar text from Resw * Add basic button UI * Add function to get default app name and to open file in default app * Add filename to titlebar * Correct error output * Remove titlebar text from Resw * Add SetDragRectangles * Correct logic, update function name * Add localization * Cleanup and adaptive width * Add fileIndex/NumberOfFiles for multiple files activation * Refine titlebar styles * Update error message; Return HResult from native methods; Update variable initialisation and string null testing * Titlebar height and adaptive width refinement * Add fallback to launch app picker if fail to open default app * Temp change to hide AppTitle_FileCount * Update launch button to command; Add keyboard accelerator * Update titlebar inactive background color * Update tooltip to add keyboard accelerator * Add comments to resw file * Fix accidental deletion from previous merge Co-authored-by: Jojo Zhou <yizzho@microsoft.com> Co-authored-by: Yawen Hou <yawenhou@microsoft.com> * Fix crash * Fix wrong thread exception * Make CurrentItemIndex setter private * Update titlebar filecount text * Fix titlebar draggable region and interactive region (bump WinAppSdk to latest) * [Peek] Unsupported File Previewer - Formatting string from resources (#22609) * Moving to string resource usage * Moving ReadableStringHelper to common project * Fix comments * [Peek] Fix foregrounding (#22633) * Fixing foregrounding * Get window handle inside BringToForeground extension method Co-authored-by: Daniel Chau <dancha@microsoft.com> Co-authored-by: Samuel Chapleau <sachaple@microsoft.com> * [Peek] ImagePreviewer - Handle error states (#22637) * add better preview state handling * add error handling in imagepreviewer and better state handling * fix error handling so exception is not bubbled up * improve performance and hook up unsupported previewer on error * remove commented code * address pr comments * [Peek] add PDF viewing support (#22636) * [Peek] add PDF viewing support * Fixed issue which would redirect some HTML and PDF files to external browser * Fixed refactored interface name * [Peek] Refine titlebar adaptive width (#22642) * Adjust adaptive width of titlebar * Remove visualstate setters for AppTitle_FileCount Co-authored-by: Jojo Zhou <yizzho@microsoft.com> * [Peek] New File Explorer tabs break Shell API to get selected files (#22641) * fix FE tab bug * remove unnecessary unsafe keyword * [Peek] add extra logic to properly render PNG files with transparency (#22613) * [Peek] added extra logic to render PNG files with proper transparency * Moved logic to ThumbnailHelper Cleanup * Created a separated previewer for PNG to only load the preview image with thumbnail logic * removed unused code * Updated state loading change * [Peek] Unsupported File Previewer - Setting Window Size (#22645) * Adding setting for unsupported file window * Fix * [Peek] Add tooltip to File (#22640) * Add tooltip to File * Add placeholder text for no tooltip * Address comments * Use StringBuilder Co-authored-by: Jojo Zhou <yizzho@microsoft.com> * Add full image quality support (#22654) * [Peek] Window foregrounding simplification and fixes + keep window visible if FE single selection changed (#22657) * Use different apis to bring to foreground removing remote thread wait and work as well as library loading * Keep window open if single selected file in FE is different * Removed unused methods * [Peek] Add cancellation token OnFilePropertyChanged (#22643) * Cancel file loading before opening another file * Add omitted cancellation checks * Catch task cancelled exception; Add more cancellation checkpoints * Add cancellation checkpoint beofre GetBitmapFromHBitmapAsync * Correct typo * Update to pass cancellation token individually to each async methods * Add lost cancellationToken source * Add cancellation token to PngPreviewer Co-authored-by: Yawen Hou <yawenhou@microsoft.com> * [Peek] Unsupported File Previewer - Preserve Transparency For File Icons (#22650) * Preserving transparency or icons * Remove TODO Co-authored-by: Samuel Chapleau <sachaple@microsoft.com> * [Peek] Update some installer build steps + assets update (#22683) * Fix settings & peek.ui.wpf * Add back missing icon * Add missing files and actions to installer * Keep window open if the selected file in explorer is different (only works for single file selection) * Undo last * [Peek] Add copy keyboard accelerator (#22647) * add copy keyboard accelerator * Fix comments Co-authored-by: Samuel Chapleau <sachaple@microsoft.com> * [Peek] add WV2 improvements (behavior and UX) (#22685) * [Peek] added logic to get max monitor size for opening WebView2 * Removed ununsed dependency property * Added workaround for cases where the web page would not finish navigating in a quick timing, for example google.com. * Remove window extensions from common and use nullable size argument instead Co-authored-by: Samuel Chapleau <sachaple@microsoft.com> * [Peek] Merge main, self-contained .NET and fix WebView2 user data dir issue (#22899) * Merge remote-tracking branch 'origin/main' into peek * Test sc * Set WebView2 user data dir * spellcheck * Fix comment * Move check if higher quality image is already loaded to the exact line where we change the Preview bitmap (#23083) * Fix opening Peek when FE window is set to full name path (#23082) * Move check for png thubmnail loading priority * Remove Peek.UI.WPF project * Remove duplicated method in powertoys setup * [Peek] Fix selecting files from the correct focused opened File Explorer tab & from Desktop (#23489) * Get file based on active tab handle instead of window title * Refactor code to get active tab * Getting all items from the shell API working again, except for desktop * Refactor and cleanup com & native code * Add back removed peek xaml assets in Product.wxs * Remove some dependencies that do not seem necessary in Product.wxs * [Peek] Small images (#23554) * change stretch value * compare with actual window size * consider scaling factor * set max size * clean up * clean up * clean up previewers * scaling factor in bitmap previewer * max image size property * [Peek]Handle errors for HEIC/HEIF and fall back to default previewer if there is no thumbnail (#22684) * Handle errors when getting filesize by falling back to default previewer * Bringing back other file types that are fixed with these code changes --------- Co-authored-by: Samuel Chapleau <sachaple@microsoft.com> * [Peek] Add unsupported file icon fallback (#23735) * Refactor icon retrieval, refactor hbitmap to bitmap conversion, add icon fallback * Add svg to assets in installer * [Peek] Refactoring of file system models, removal of PngPreviewer, retrieving of folder size via Scripting com reference and other fixes (#23955) * Refactor icon retrieval, refactor hbitmap to bitmap conversion, add icon fallback * Add svg to assets in installer * - Refactor File class into IFileSystemItem, FileItem & FolderItem - Display size for folders using Scripting namespace - Remove default app buttons for files or folders not supporting it * Add better content type via storage apis * Add check for storagefile in PngPreviewer * Fix png stretching * Remove png previewer * Rename ThumbnailOptions.None to ThumbnailOptions.ResizeToFit * [Peek] Removed monitor percentage evaluation for the UnsupportedFilePreview control (#24002) * Remove settings for percentage of windows and keep default min size. * Fix margin on unsupported control * Use nullable Size for image size & open file on background thread (#24004) * [Peek] SVG support (#24237) * svg previewer * svg size * set scaling factor * set image size * changed image source type * non nullable image size * notify svg previewer changed * uncomment * rename BitmapPreviewer * move svg support * remove svg previewer * [Peek] Implementation of a performant and reliable Neighboring Files Query (#24943) * Use IShellItemArray as the backing array of item * Finalize and cleanup NFQ implementation * Cleanup remainder of the code * Remove unused using * [Peek] Pin the window position (#24927) * [Peek] Telemetry and logging (#25231) * text preview * scrolling * changed size * webview2 preview * common preview project * previewpane: use common project * peek: use common * previewpane: moved md * peek: md * previewpane: clean up * clean up * moved monaco files * moved formatters * rename * moved common monaco helper * dev files support * installer * removed versions * warnings: culture info * warnings: names * clean up * warnings: dispose * warnings: default values * warnings * warnings: charset * warnings: exceptions * suppress warning * installer: added peek * changed peek guid * monaco folders * peek deps * peek files * peek resources * removed additional monaco folder * set host name * Update installer * hardcode monaco path * leave single webview control * moved path to common * project * more meaningful todos * moved temp folder cleanup * todo * extension check * spell: monaco * spellcheck * spellcheck * fix id * fix spelling * key to spelling * id fix * Fix monaco resolution at install time * Fix user install. Add needed files * installer: remove peek localization files. It's a WinUI app * installer:fix signing * removed unused * settings: flyout enable/disable for Peek * simplify string * property changed handle * [Peek][Settings] Peek OOBE page (#25895) * [Peek] GPO (#25918) * Add Native methods file to exception * Fix merge issue on solution file * Adjust spellcheck * Remove boilerplate code * Add module interface telemetry * Remove change to README.md * Add entry to README * Clean up some non-changes * Fix order of Peek in Settings menu * [Settings] Make peek descriptions more descriptive --------- Co-authored-by: Michael Salmon <miksalmon@users.noreply.github.com> Co-authored-by: Michael Salmon 🐟 <michaelpsalmon@outlook.com> Co-authored-by: Alireza Ebadi Ghajari <alirezae@microsoft.com> Co-authored-by: Jessie Su <Jessie.Su@microsoft.com> Co-authored-by: sujessie <102062556+sujessie@users.noreply.github.com> Co-authored-by: Daniel Chau <d.chau@alumni.ubc.ca> Co-authored-by: Daniel Chau <dancha@microsoft.com> Co-authored-by: jth-ms <73617023+jth-ms@users.noreply.github.com> Co-authored-by: Robson <rp.pontin@gmail.com> Co-authored-by: estebanm123 <49930791+estebanm123@users.noreply.github.com> Co-authored-by: Esteban Margaron <emargaron@microsoft.com> Co-authored-by: Yawen Hou <Sytta@users.noreply.github.com> Co-authored-by: Jojo Zhou <yizzho@microsoft.com> Co-authored-by: Yawen Hou <yawenhou@microsoft.com> Co-authored-by: Jojo Zhou <39350350+Joanna-Zhou@users.noreply.github.com> Co-authored-by: Stefan Markovic <57057282+stefansjfw@users.noreply.github.com> Co-authored-by: Seraphima Zykova <zykovas91@gmail.com> Co-authored-by: Stefan Markovic <stefan@janeasystems.com> Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||
<!-- Licensed under the MIT License. See LICENSE in the project root for license information. -->
|
||||
|
||||
<UserControl
|
||||
x:Class="Peek.FilePreviewer.Controls.BrowserControl"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:Peek.FilePreviewer.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<controls:WebView2 x:Name="PreviewBrowser"
|
||||
Loaded="PreviewWV2_Loaded"
|
||||
NavigationStarting="PreviewBrowser_NavigationStarting"
|
||||
NavigationCompleted="PreviewWV2_NavigationCompleted"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Peek.Common.Constants;
|
||||
using Peek.Common.Helpers;
|
||||
using Windows.System;
|
||||
|
||||
namespace Peek.FilePreviewer.Controls
|
||||
{
|
||||
public sealed partial class BrowserControl : UserControl, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper private Uri where we cache the last navigated page
|
||||
/// so we can redirect internal PDF or Webpage links to external
|
||||
/// web browser, avoiding WebView internal navigation.
|
||||
/// </summary>
|
||||
private Uri? _navigatedUri;
|
||||
|
||||
public delegate void NavigationCompletedHandler(WebView2? sender, CoreWebView2NavigationCompletedEventArgs? args);
|
||||
|
||||
public delegate void DOMContentLoadedHandler(CoreWebView2? sender, CoreWebView2DOMContentLoadedEventArgs? args);
|
||||
|
||||
public event NavigationCompletedHandler? NavigationCompleted;
|
||||
|
||||
public event DOMContentLoadedHandler? DOMContentLoaded;
|
||||
|
||||
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
|
||||
nameof(Source),
|
||||
typeof(Uri),
|
||||
typeof(BrowserControl),
|
||||
new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((BrowserControl)d).SourcePropertyChanged())));
|
||||
|
||||
public Uri? Source
|
||||
{
|
||||
get { return (Uri)GetValue(SourceProperty); }
|
||||
set { SetValue(SourceProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty IsDevFilePreviewProperty = DependencyProperty.Register(
|
||||
nameof(IsDevFilePreview),
|
||||
typeof(bool),
|
||||
typeof(BrowserControl),
|
||||
new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((BrowserControl)d).OnIsDevFilePreviewChanged())));
|
||||
|
||||
public bool IsDevFilePreview
|
||||
{
|
||||
get
|
||||
{
|
||||
return (bool)GetValue(IsDevFilePreviewProperty);
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
SetValue(IsDevFilePreviewProperty, value);
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserControl()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", TempFolderPath.Path, EnvironmentVariableTarget.Process);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (PreviewBrowser.CoreWebView2 != null)
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.DOMContentLoaded -= CoreWebView2_DOMContentLoaded;
|
||||
}
|
||||
|
||||
Microsoft.PowerToys.FilePreviewCommon.Helper.CleanupTempDir(TempFolderPath.Path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigate to the to the <see cref="Uri"/> set in <see cref="Source"/>.
|
||||
/// Calling <see cref="Navigate"/> will always trigger a navigation/refresh
|
||||
/// even if web target file is the same.
|
||||
/// </summary>
|
||||
public void Navigate()
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable("WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS");
|
||||
|
||||
_navigatedUri = null;
|
||||
|
||||
if (Source != null && PreviewBrowser.CoreWebView2 != null)
|
||||
{
|
||||
/* CoreWebView2.Navigate() will always trigger a navigation even if the content/URI is the same.
|
||||
* Use WebView2.Source to avoid re-navigating to the same content. */
|
||||
PreviewBrowser.CoreWebView2.Navigate(Source.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private void SourcePropertyChanged()
|
||||
{
|
||||
Navigate();
|
||||
}
|
||||
|
||||
private void OnIsDevFilePreviewChanged()
|
||||
{
|
||||
if (PreviewBrowser.CoreWebView2 != null)
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = IsDevFilePreview;
|
||||
if (IsDevFilePreview)
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void PreviewWV2_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
await PreviewBrowser.EnsureCoreWebView2Async();
|
||||
|
||||
PreviewBrowser.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = false;
|
||||
PreviewBrowser.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
|
||||
PreviewBrowser.CoreWebView2.Settings.AreDevToolsEnabled = false;
|
||||
PreviewBrowser.CoreWebView2.Settings.AreHostObjectsAllowed = false;
|
||||
PreviewBrowser.CoreWebView2.Settings.IsGeneralAutofillEnabled = false;
|
||||
PreviewBrowser.CoreWebView2.Settings.IsPasswordAutosaveEnabled = false;
|
||||
PreviewBrowser.CoreWebView2.Settings.IsScriptEnabled = IsDevFilePreview;
|
||||
PreviewBrowser.CoreWebView2.Settings.IsWebMessageEnabled = false;
|
||||
|
||||
if (IsDevFilePreview)
|
||||
{
|
||||
PreviewBrowser.CoreWebView2.SetVirtualHostNameToFolderMapping(Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.MonacoDirectory, CoreWebView2HostResourceAccessKind.Allow);
|
||||
}
|
||||
|
||||
PreviewBrowser.CoreWebView2.DOMContentLoaded += CoreWebView2_DOMContentLoaded;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("WebView2 loading failed. " + ex.Message);
|
||||
}
|
||||
|
||||
Navigate();
|
||||
}
|
||||
|
||||
private void CoreWebView2_DOMContentLoaded(CoreWebView2 sender, CoreWebView2DOMContentLoadedEventArgs args)
|
||||
{
|
||||
DOMContentLoaded?.Invoke(sender, args);
|
||||
}
|
||||
|
||||
private async void PreviewBrowser_NavigationStarting(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs args)
|
||||
{
|
||||
if (_navigatedUri == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// In case user starts or tries to navigate from within the HTML file we launch default web browser for navigation.
|
||||
if (args.Uri != null && args.Uri != _navigatedUri?.ToString() && args.IsUserInitiated)
|
||||
{
|
||||
args.Cancel = true;
|
||||
await Launcher.LaunchUriAsync(new Uri(args.Uri));
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewWV2_NavigationCompleted(WebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs args)
|
||||
{
|
||||
if (args.IsSuccess)
|
||||
{
|
||||
_navigatedUri = Source;
|
||||
}
|
||||
|
||||
NavigationCompleted?.Invoke(sender, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<UserControl
|
||||
x:Class="Peek.FilePreviewer.Controls.UnsupportedFilePreview"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid
|
||||
Margin="48"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition x:Name="Icon" Width="Auto" />
|
||||
<ColumnDefinition x:Name="FileInfo" Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Image
|
||||
x:Name="PreviewImage"
|
||||
Grid.Column="0"
|
||||
Width="180"
|
||||
Height="180"
|
||||
Margin="0,24,24,24"
|
||||
Source="{x:Bind IconPreview, Mode=OneWay}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="5">
|
||||
|
||||
<TextBlock
|
||||
FontSize="26"
|
||||
FontWeight="SemiBold"
|
||||
Text="{x:Bind FileName, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{x:Bind FormattedFileType, Mode=OneWay}" />
|
||||
<TextBlock Text="{x:Bind FormattedFileSize, Mode=OneWay}" />
|
||||
<TextBlock Text="{x:Bind FormattedDateModified, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,44 @@
|
||||
// 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.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Peek.Common.Helpers;
|
||||
|
||||
namespace Peek.FilePreviewer.Controls
|
||||
{
|
||||
[INotifyPropertyChanged]
|
||||
public sealed partial class UnsupportedFilePreview : UserControl
|
||||
{
|
||||
[ObservableProperty]
|
||||
private ImageSource? iconPreview;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? fileName;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FormattedFileType))]
|
||||
private string? fileType;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FormattedFileSize))]
|
||||
private string? fileSize;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(FormattedDateModified))]
|
||||
private string? dateModified;
|
||||
|
||||
public string FormattedFileType => ReadableStringHelper.FormatResourceString("UnsupportedFile_FileType", FileType);
|
||||
|
||||
public string FormattedFileSize => ReadableStringHelper.FormatResourceString("UnsupportedFile_FileSize", FileSize);
|
||||
|
||||
public string FormattedDateModified => ReadableStringHelper.FormatResourceString("UnsupportedFile_DateModified", DateModified);
|
||||
|
||||
public UnsupportedFilePreview()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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;
|
||||
|
||||
namespace Peek.FilePreviewer.Exceptions
|
||||
{
|
||||
public class ImageLoadingException : Exception
|
||||
{
|
||||
public ImageLoadingException()
|
||||
{
|
||||
}
|
||||
|
||||
public ImageLoadingException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ImageLoadingException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
53
src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
Normal file
53
src/modules/peek/Peek.FilePreviewer/FilePreview.xaml
Normal file
@@ -0,0 +1,53 @@
|
||||
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
|
||||
<UserControl
|
||||
x:Class="Peek.FilePreviewer.FilePreview"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Peek.FilePreviewer.Controls"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="using:Peek.FilePreviewer"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:previewers="using:Peek.FilePreviewer.Previewers"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Grid>
|
||||
<ProgressRing
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsActive="{x:Bind MatchPreviewState(Previewer.State, previewers:PreviewState.Loading), Mode=OneWay}" />
|
||||
|
||||
<Image
|
||||
x:Name="ImagePreview"
|
||||
MaxWidth="{x:Bind ImagePreviewer.MaxImageSize.Width, Mode=OneWay}"
|
||||
MaxHeight="{x:Bind ImagePreviewer.MaxImageSize.Height, Mode=OneWay}"
|
||||
Source="{x:Bind ImagePreviewer.Preview, Mode=OneWay}"
|
||||
ToolTipService.ToolTip="{x:Bind ImageInfoTooltip, Mode=OneWay}"
|
||||
Visibility="{x:Bind IsPreviewVisible(ImagePreviewer, Previewer.State), Mode=OneWay}" />
|
||||
|
||||
<controls:BrowserControl
|
||||
x:Name="BrowserPreview"
|
||||
x:Load="True"
|
||||
DOMContentLoaded="BrowserPreview_DOMContentLoaded"
|
||||
NavigationCompleted="PreviewBrowser_NavigationCompleted"
|
||||
Source="{x:Bind BrowserPreviewer.Preview, Mode=OneWay}"
|
||||
IsDevFilePreview="{x:Bind BrowserPreviewer.IsDevFilePreview, Mode=OneWay}"
|
||||
Visibility="{x:Bind IsPreviewVisible(BrowserPreviewer, Previewer.State), Mode=OneWay, FallbackValue=Collapsed}" />
|
||||
|
||||
<controls:UnsupportedFilePreview
|
||||
x:Name="UnsupportedFilePreview"
|
||||
DateModified="{x:Bind UnsupportedFilePreviewer.DateModified, Mode=OneWay}"
|
||||
FileName="{x:Bind UnsupportedFilePreviewer.FileName, Mode=OneWay}"
|
||||
FileSize="{x:Bind UnsupportedFilePreviewer.FileSize, Mode=OneWay}"
|
||||
FileType="{x:Bind UnsupportedFilePreviewer.FileType, Mode=OneWay}"
|
||||
IconPreview="{x:Bind UnsupportedFilePreviewer.IconPreview, Mode=OneWay}"
|
||||
Visibility="{x:Bind IsPreviewVisible(UnsupportedFilePreviewer, Previewer.State), Mode=OneWay}" />
|
||||
</Grid>
|
||||
<UserControl.KeyboardAccelerators>
|
||||
<KeyboardAccelerator
|
||||
Key="C"
|
||||
Invoked="KeyboardAccelerator_CtrlC_Invoked"
|
||||
Modifiers="Control" />
|
||||
</UserControl.KeyboardAccelerators>
|
||||
</UserControl>
|
||||
275
src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
Normal file
275
src/modules/peek/Peek.FilePreviewer/FilePreview.xaml.cs
Normal file
@@ -0,0 +1,275 @@
|
||||
// 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.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.Common.Models;
|
||||
using Peek.FilePreviewer.Models;
|
||||
using Peek.FilePreviewer.Previewers;
|
||||
using Peek.FilePreviewer.Previewers.Interfaces;
|
||||
using Peek.UI.Telemetry.Events;
|
||||
using Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace Peek.FilePreviewer
|
||||
{
|
||||
[INotifyPropertyChanged]
|
||||
public sealed partial class FilePreview : UserControl, IDisposable
|
||||
{
|
||||
private readonly PreviewerFactory previewerFactory = new();
|
||||
|
||||
public event EventHandler<PreviewSizeChangedArgs>? PreviewSizeChanged;
|
||||
|
||||
public static readonly DependencyProperty ItemProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(Item),
|
||||
typeof(IFileSystemItem),
|
||||
typeof(FilePreview),
|
||||
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnItemPropertyChanged()));
|
||||
|
||||
public static readonly DependencyProperty ScalingFactorProperty =
|
||||
DependencyProperty.Register(
|
||||
nameof(ScalingFactor),
|
||||
typeof(double),
|
||||
typeof(FilePreview),
|
||||
new PropertyMetadata(false, async (d, e) => await ((FilePreview)d).OnScalingFactorPropertyChanged()));
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ImagePreviewer))]
|
||||
[NotifyPropertyChangedFor(nameof(BrowserPreviewer))]
|
||||
[NotifyPropertyChangedFor(nameof(UnsupportedFilePreviewer))]
|
||||
|
||||
private IPreviewer? previewer;
|
||||
|
||||
[ObservableProperty]
|
||||
private string imageInfoTooltip = ResourceLoader.GetForViewIndependentUse().GetString("PreviewTooltip_Blank");
|
||||
|
||||
private CancellationTokenSource _cancellationTokenSource = new();
|
||||
|
||||
public FilePreview()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_cancellationTokenSource.Dispose();
|
||||
}
|
||||
|
||||
private async void Previewer_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
// Fallback on DefaultPreviewer if we fail to load the correct Preview
|
||||
if (e.PropertyName == nameof(IPreviewer.State))
|
||||
{
|
||||
if (Previewer?.State == PreviewState.Error)
|
||||
{
|
||||
// Cancel previous loading task
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
Previewer = previewerFactory.CreateDefaultPreviewer(Item);
|
||||
await UpdatePreviewAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IImagePreviewer? ImagePreviewer => Previewer as IImagePreviewer;
|
||||
|
||||
public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer;
|
||||
|
||||
public IUnsupportedFilePreviewer? UnsupportedFilePreviewer => Previewer as IUnsupportedFilePreviewer;
|
||||
|
||||
public IFileSystemItem Item
|
||||
{
|
||||
get => (IFileSystemItem)GetValue(ItemProperty);
|
||||
set => SetValue(ItemProperty, value);
|
||||
}
|
||||
|
||||
public double ScalingFactor
|
||||
{
|
||||
get => (double)GetValue(ScalingFactorProperty);
|
||||
set
|
||||
{
|
||||
SetValue(ScalingFactorProperty, value);
|
||||
|
||||
if (Previewer is IImagePreviewer imagePreviewer)
|
||||
{
|
||||
imagePreviewer.ScalingFactor = ScalingFactor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchPreviewState(PreviewState? value, PreviewState stateToMatch)
|
||||
{
|
||||
return value == stateToMatch;
|
||||
}
|
||||
|
||||
public Visibility IsPreviewVisible(IPreviewer? previewer, PreviewState? state)
|
||||
{
|
||||
var isValidPreview = previewer != null && MatchPreviewState(state, PreviewState.Loaded);
|
||||
return isValidPreview ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async Task OnItemPropertyChanged()
|
||||
{
|
||||
// Cancel previous loading task
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
if (Item == null)
|
||||
{
|
||||
Previewer = null;
|
||||
ImagePreview.Visibility = Visibility.Collapsed;
|
||||
BrowserPreview.Visibility = Visibility.Collapsed;
|
||||
UnsupportedFilePreview.Visibility = Visibility.Collapsed;
|
||||
return;
|
||||
}
|
||||
|
||||
Previewer = previewerFactory.Create(Item);
|
||||
if (Previewer is IImagePreviewer imagePreviewer)
|
||||
{
|
||||
imagePreviewer.ScalingFactor = ScalingFactor;
|
||||
}
|
||||
|
||||
await UpdatePreviewAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private async Task OnScalingFactorPropertyChanged()
|
||||
{
|
||||
// Cancel previous loading task
|
||||
_cancellationTokenSource.Cancel();
|
||||
_cancellationTokenSource = new();
|
||||
|
||||
await UpdatePreviewAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
private async Task UpdatePreviewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Previewer != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var size = await Previewer.GetPreviewSizeAsync(cancellationToken);
|
||||
PreviewSizeChanged?.Invoke(this, new PreviewSizeChangedArgs(size));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Previewer.LoadPreviewAsync(cancellationToken);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await UpdateImageTooltipAsync(cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// TODO: Log task cancelled exception?
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Fall back to Default previewer
|
||||
PowerToysTelemetry.Log.WriteEvent(new ErrorEvent() { HResult = (Common.Models.HResult)ex.HResult, Message = ex.Message, Failure = ErrorEvent.FailureType.PreviewFail });
|
||||
Logger.LogError("Error in UpdatePreviewAsync, falling back to default previewer: " + ex.Message);
|
||||
Previewer.State = PreviewState.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnPreviewerChanging(IPreviewer? value)
|
||||
{
|
||||
if (Previewer != null)
|
||||
{
|
||||
Previewer.PropertyChanged -= Previewer_PropertyChanged;
|
||||
}
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
value.PropertyChanged += Previewer_PropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void BrowserPreview_DOMContentLoaded(Microsoft.Web.WebView2.Core.CoreWebView2 sender, Microsoft.Web.WebView2.Core.CoreWebView2DOMContentLoadedEventArgs args)
|
||||
{
|
||||
/*
|
||||
* There is an odd behavior where the WebView2 would not raise the NavigationCompleted event
|
||||
* for certain HTML files, even though it has already been loaded. Probably related to certain
|
||||
* extra module that require more time to load. One example is saving and opening google.com locally.
|
||||
*
|
||||
* So to address this, we will make the Browser visible and display it as "Loaded" as soon the HTML document
|
||||
* has been parsed and loaded with the DOMContentLoaded event.
|
||||
*
|
||||
* Similar issue: https://github.com/MicrosoftEdge/WebView2Feedback/issues/998
|
||||
*/
|
||||
if (BrowserPreviewer != null)
|
||||
{
|
||||
BrowserPreviewer.State = PreviewState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
private void PreviewBrowser_NavigationCompleted(WebView2 sender, CoreWebView2NavigationCompletedEventArgs args)
|
||||
{
|
||||
/*
|
||||
* In theory most of navigation should work after DOM is loaded.
|
||||
* But in case something fails we check NavigationCompleted event
|
||||
* for failure and switch visibility accordingly.
|
||||
*
|
||||
* As an alternative, in the future, the preview Browser control
|
||||
* could also display error content.
|
||||
*/
|
||||
if (!args.IsSuccess)
|
||||
{
|
||||
if (BrowserPreviewer != null)
|
||||
{
|
||||
BrowserPreviewer.State = PreviewState.Error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void KeyboardAccelerator_CtrlC_Invoked(KeyboardAccelerator sender, KeyboardAcceleratorInvokedEventArgs args)
|
||||
{
|
||||
if (Previewer != null)
|
||||
{
|
||||
await Previewer.CopyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch and format available file properties
|
||||
var sb = new StringBuilder();
|
||||
|
||||
string fileNameFormatted = ReadableStringHelper.FormatResourceString("PreviewTooltip_FileName", Item.Name);
|
||||
sb.Append(fileNameFormatted);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
string fileType = await Task.Run(Item.GetContentTypeAsync);
|
||||
string fileTypeFormatted = string.IsNullOrEmpty(fileType) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileType", fileType);
|
||||
sb.Append(fileTypeFormatted);
|
||||
|
||||
string dateModified = Item.DateModified.ToString(CultureInfo.CurrentCulture);
|
||||
string dateModifiedFormatted = string.IsNullOrEmpty(dateModified) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_DateModified", dateModified);
|
||||
sb.Append(dateModifiedFormatted);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ulong bytes = await Task.Run(Item.GetSizeInBytes);
|
||||
string fileSize = ReadableStringHelper.BytesToReadableString(bytes);
|
||||
string fileSizeFormatted = string.IsNullOrEmpty(fileSize) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileSize", fileSize);
|
||||
sb.Append(fileSizeFormatted);
|
||||
|
||||
ImageInfoTooltip = sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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 Windows.Foundation;
|
||||
|
||||
namespace Peek.FilePreviewer.Models
|
||||
{
|
||||
public class PreviewSizeChangedArgs
|
||||
{
|
||||
public PreviewSizeChangedArgs(Size? windowSizeRequested)
|
||||
{
|
||||
WindowSizeRequested = windowSizeRequested;
|
||||
}
|
||||
|
||||
public Size? WindowSizeRequested { get; init; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\Version.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>Peek.FilePreviewer</RootNamespace>
|
||||
<RuntimeIdentifiers>win10-x86;win10-x64;win10-arm64</RuntimeIdentifiers>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Controls\BrowserControl.xaml" />
|
||||
<None Remove="FilePreview.xaml" />
|
||||
<None Remove="UnsupportedFilePreview.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\FilePreviewCommon\FilePreviewCommon.csproj" />
|
||||
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
|
||||
<ProjectReference Include="..\Peek.Common\Peek.Common.csproj" />
|
||||
<ProjectReference Include="..\WIC\WIC.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="FilePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="UnsupportedFilePreview.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Update="Controls\BrowserControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Previewers\DrivePreviewer\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Peek.Common;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers.Helpers
|
||||
{
|
||||
public static class BitmapHelper
|
||||
{
|
||||
public static async Task<BitmapSource> GetBitmapFromHBitmapAsync(IntPtr hbitmap, bool isSupportingTransparency, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var bitmap = System.Drawing.Image.FromHbitmap(hbitmap);
|
||||
if (isSupportingTransparency)
|
||||
{
|
||||
bitmap.MakeTransparent();
|
||||
}
|
||||
|
||||
var bitmapImage = new BitmapImage();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
bitmap.Save(stream, isSupportingTransparency ? ImageFormat.Png : ImageFormat.Bmp);
|
||||
stream.Position = 0;
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream());
|
||||
}
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// delete HBitmap to avoid memory leaks
|
||||
NativeMethods.DeleteObject(hbitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Peek.Common;
|
||||
using Peek.Common.Models;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers.Helpers
|
||||
{
|
||||
public static class IconHelper
|
||||
{
|
||||
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
||||
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
|
||||
|
||||
public static async Task<ImageSource?> GetIconAsync(string fileName, CancellationToken cancellationToken)
|
||||
{
|
||||
ImageSource? imageSource = null;
|
||||
IShellItem? nativeShellItem = null;
|
||||
try
|
||||
{
|
||||
Guid shellItem2Guid = new(IShellItem2Guid);
|
||||
int retCode = NativeMethods.SHCreateItemFromParsingName(fileName, IntPtr.Zero, ref shellItem2Guid, out nativeShellItem);
|
||||
|
||||
if (retCode != 0)
|
||||
{
|
||||
throw Marshal.GetExceptionForHR(retCode)!;
|
||||
}
|
||||
|
||||
NativeSize large = new NativeSize { Width = 256, Height = 256 };
|
||||
var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.IconOnly;
|
||||
|
||||
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(large, options, out IntPtr hbitmap);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (hr == HResult.Ok)
|
||||
{
|
||||
imageSource = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, true, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
var svgImageSource = new SvgImageSource(new Uri("ms-appx:///Assets/DefaultFileIcon.svg"));
|
||||
imageSource = svgImageSource;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (nativeShellItem != null)
|
||||
{
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
}
|
||||
}
|
||||
|
||||
return imageSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using Peek.Common.Models;
|
||||
|
||||
namespace Peek.Common
|
||||
{
|
||||
public static class NativeMethods
|
||||
{
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern int SHCreateItemFromParsingName(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string path,
|
||||
IntPtr pbc,
|
||||
ref Guid riid,
|
||||
[MarshalAs(UnmanagedType.Interface)] out IShellItem shellItem);
|
||||
|
||||
[DllImport("gdi32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static extern bool DeleteObject(IntPtr hObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Peek.Common;
|
||||
using Peek.Common.Models;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public static class ThumbnailHelper
|
||||
{
|
||||
// Based on https://stackoverflow.com/questions/21751747/extract-thumbnail-for-any-file-in-windows
|
||||
private const string IShellItem2Guid = "7E9FB0D3-919F-4307-AB2E-9B1860310C93";
|
||||
|
||||
public static readonly NativeSize HighQualityThumbnailSize = new NativeSize { Width = 720, Height = 720, };
|
||||
public static readonly NativeSize LowQualityThumbnailSize = new NativeSize { Width = 256, Height = 256, };
|
||||
|
||||
private static readonly NativeSize FallBackThumbnailSize = new NativeSize { Width = 96, Height = 96, };
|
||||
private static readonly NativeSize LastFallBackThumbnailSize = new NativeSize { Width = 32, Height = 32, };
|
||||
|
||||
private static readonly List<NativeSize> ThumbnailFallBackSizes = new List<NativeSize>
|
||||
{
|
||||
HighQualityThumbnailSize,
|
||||
LowQualityThumbnailSize,
|
||||
FallBackThumbnailSize,
|
||||
LastFallBackThumbnailSize,
|
||||
};
|
||||
|
||||
// TODO: Add a re-try system if there is no thumbnail of requested size.
|
||||
public static HResult GetThumbnail(string filename, out IntPtr hbitmap, NativeSize thumbnailSize)
|
||||
{
|
||||
Guid shellItem2Guid = new Guid(IShellItem2Guid);
|
||||
int retCode = NativeMethods.SHCreateItemFromParsingName(filename, IntPtr.Zero, ref shellItem2Guid, out IShellItem nativeShellItem);
|
||||
|
||||
if (retCode != 0)
|
||||
{
|
||||
throw Marshal.GetExceptionForHR(retCode)!;
|
||||
}
|
||||
|
||||
var options = ThumbnailOptions.BiggerSizeOk | ThumbnailOptions.ThumbnailOnly | ThumbnailOptions.ScaleUp;
|
||||
|
||||
HResult hr = ((IShellItemImageFactory)nativeShellItem).GetImage(thumbnailSize, options, out hbitmap);
|
||||
|
||||
// Try to get thumbnail using the fallback sizes order
|
||||
if (hr != HResult.Ok)
|
||||
{
|
||||
var currentThumbnailFallBackIndex = ThumbnailFallBackSizes.IndexOf(thumbnailSize);
|
||||
var nextThumbnailFallBackIndex = currentThumbnailFallBackIndex + 1;
|
||||
if (nextThumbnailFallBackIndex < ThumbnailFallBackSizes.Count - 1)
|
||||
{
|
||||
hr = GetThumbnail(filename, out hbitmap, ThumbnailFallBackSizes[nextThumbnailFallBackIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.ReleaseComObject(nativeShellItem);
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
public static async Task<BitmapImage?> GetThumbnailAsync(StorageFile? storageFile, uint size)
|
||||
{
|
||||
BitmapImage? bitmapImage = null;
|
||||
|
||||
var imageStream = await storageFile?.GetThumbnailAsync(
|
||||
Windows.Storage.FileProperties.ThumbnailMode.SingleItem,
|
||||
size,
|
||||
Windows.Storage.FileProperties.ThumbnailOptions.None);
|
||||
|
||||
if (imageStream == null)
|
||||
{
|
||||
return bitmapImage;
|
||||
}
|
||||
|
||||
bitmapImage = new BitmapImage();
|
||||
bitmapImage.SetSource(imageStream);
|
||||
|
||||
return bitmapImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.Threading.Tasks;
|
||||
using WIC;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public static class WICHelper
|
||||
{
|
||||
public static Task<Windows.Foundation.Size> GetImageSize(string filePath)
|
||||
{
|
||||
return Task.Run(() =>
|
||||
{
|
||||
// TODO: Find a way to get file metadata without hydrating files. Look into Shell API/Windows Property System, e.g., IPropertyStore
|
||||
IWICImagingFactory factory = (IWICImagingFactory)new WICImagingFactoryClass();
|
||||
var decoder = factory.CreateDecoderFromFilename(filePath, IntPtr.Zero, StreamAccessMode.GENERIC_READ, WICDecodeOptions.WICDecodeMetadataCacheOnLoad);
|
||||
var frame = decoder?.GetFrame(0);
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
// TODO: Respect EXIF data and find correct orientation
|
||||
frame?.GetSize(out width, out height);
|
||||
|
||||
return new Windows.Foundation.Size(width, height);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.Common.Models;
|
||||
using Peek.FilePreviewer.Exceptions;
|
||||
using Peek.FilePreviewer.Previewers.Helpers;
|
||||
using Peek.FilePreviewer.Previewers.Interfaces;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public partial class ImagePreviewer : ObservableObject, IImagePreviewer, IDisposable
|
||||
{
|
||||
[ObservableProperty]
|
||||
private ImageSource? preview;
|
||||
|
||||
[ObservableProperty]
|
||||
private PreviewState state;
|
||||
|
||||
[ObservableProperty]
|
||||
private Size? imageSize;
|
||||
|
||||
[ObservableProperty]
|
||||
private Size maxImageSize;
|
||||
|
||||
[ObservableProperty]
|
||||
private double scalingFactor;
|
||||
|
||||
public ImagePreviewer(IFileSystemItem file)
|
||||
{
|
||||
Item = file;
|
||||
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
private IFileSystemItem Item { get; }
|
||||
|
||||
private DispatcherQueue Dispatcher { get; }
|
||||
|
||||
private Task<bool>? LowQualityThumbnailTask { get; set; }
|
||||
|
||||
private Task<bool>? HighQualityThumbnailTask { get; set; }
|
||||
|
||||
private Task<bool>? FullQualityImageTask { get; set; }
|
||||
|
||||
private bool IsHighQualityThumbnailLoaded => HighQualityThumbnailTask?.Status == TaskStatus.RanToCompletion;
|
||||
|
||||
private bool IsFullImageLoaded => FullQualityImageTask?.Status == TaskStatus.RanToCompletion;
|
||||
|
||||
public static bool IsFileTypeSupported(string fileExt)
|
||||
{
|
||||
return _supportedFileTypes.Contains(fileExt);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (IsSvg(Item))
|
||||
{
|
||||
var size = await Task.Run(Item.GetSvgSize);
|
||||
if (size != null)
|
||||
{
|
||||
ImageSize = size.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImageSize = await Task.Run(Item.GetImageSize);
|
||||
if (ImageSize == null)
|
||||
{
|
||||
ImageSize = await WICHelper.GetImageSize(Item.Path);
|
||||
}
|
||||
}
|
||||
|
||||
return ImageSize;
|
||||
}
|
||||
|
||||
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
State = PreviewState.Loading;
|
||||
|
||||
LowQualityThumbnailTask = LoadLowQualityThumbnailAsync(cancellationToken);
|
||||
HighQualityThumbnailTask = LoadHighQualityThumbnailAsync(cancellationToken);
|
||||
FullQualityImageTask = LoadFullQualityImageAsync(cancellationToken);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Task.WhenAll(LowQualityThumbnailTask, HighQualityThumbnailTask, FullQualityImageTask);
|
||||
|
||||
if (Preview == null && HasFailedLoadingPreview())
|
||||
{
|
||||
State = PreviewState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CopyAsync()
|
||||
{
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
var storageItem = await Item.GetStorageItemAsync();
|
||||
ClipboardHelper.SaveToClipboard(storageItem);
|
||||
});
|
||||
}
|
||||
|
||||
partial void OnPreviewChanged(ImageSource? value)
|
||||
{
|
||||
if (Preview != null)
|
||||
{
|
||||
State = PreviewState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnScalingFactorChanged(double value)
|
||||
{
|
||||
UpdateMaxImageSize();
|
||||
}
|
||||
|
||||
partial void OnImageSizeChanged(Size? value)
|
||||
{
|
||||
UpdateMaxImageSize();
|
||||
}
|
||||
|
||||
private void UpdateMaxImageSize()
|
||||
{
|
||||
var imageWidth = ImageSize?.Width ?? 0;
|
||||
var imageHeight = ImageSize?.Height ?? 0;
|
||||
|
||||
if (ScalingFactor != 0)
|
||||
{
|
||||
MaxImageSize = new Size(imageWidth / ScalingFactor, imageHeight / ScalingFactor);
|
||||
}
|
||||
else
|
||||
{
|
||||
MaxImageSize = new Size(imageWidth, imageHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private Task<bool> LoadLowQualityThumbnailAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TaskExtension.RunSafe(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(Item.Path), out IntPtr hbitmap, ThumbnailHelper.LowQualityThumbnailSize);
|
||||
if (hr != HResult.Ok)
|
||||
{
|
||||
Logger.LogError("Error loading low quality thumbnail - hresult: " + hr);
|
||||
throw new ImageLoadingException(nameof(hbitmap));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, IsPng(Item), cancellationToken);
|
||||
if (!IsFullImageLoaded && !IsHighQualityThumbnailLoaded)
|
||||
{
|
||||
Preview = thumbnailBitmap;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Task<bool> LoadHighQualityThumbnailAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TaskExtension.RunSafe(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var hr = ThumbnailHelper.GetThumbnail(Path.GetFullPath(Item.Path), out IntPtr hbitmap, ThumbnailHelper.HighQualityThumbnailSize);
|
||||
if (hr != HResult.Ok)
|
||||
{
|
||||
Logger.LogError("Error loading high quality thumbnail - hresult: " + hr);
|
||||
throw new ImageLoadingException(nameof(hbitmap));
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var thumbnailBitmap = await BitmapHelper.GetBitmapFromHBitmapAsync(hbitmap, IsPng(Item), cancellationToken);
|
||||
if (!IsFullImageLoaded)
|
||||
{
|
||||
Preview = thumbnailBitmap;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private Task<bool> LoadFullQualityImageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TaskExtension.RunSafe(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
using FileStream stream = File.OpenRead(Item.Path);
|
||||
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (IsSvg(Item))
|
||||
{
|
||||
var source = new SvgImageSource();
|
||||
source.RasterizePixelHeight = ImageSize?.Height ?? 0;
|
||||
source.RasterizePixelWidth = ImageSize?.Width ?? 0;
|
||||
|
||||
var loadStatus = await source.SetSourceAsync(stream.AsRandomAccessStream());
|
||||
if (loadStatus != SvgImageSourceLoadStatus.Success)
|
||||
{
|
||||
Logger.LogError("Error loading SVG: " + loadStatus.ToString());
|
||||
throw new ImageLoadingException(nameof(source));
|
||||
}
|
||||
|
||||
Preview = source;
|
||||
}
|
||||
else
|
||||
{
|
||||
var bitmap = new BitmapImage();
|
||||
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
|
||||
Preview = bitmap;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private bool HasFailedLoadingPreview()
|
||||
{
|
||||
var hasFailedLoadingLowQualityThumbnail = !(LowQualityThumbnailTask?.Result ?? true);
|
||||
var hasFailedLoadingHighQualityThumbnail = !(HighQualityThumbnailTask?.Result ?? true);
|
||||
var hasFailedLoadingFullQualityImage = !(FullQualityImageTask?.Result ?? true);
|
||||
|
||||
return hasFailedLoadingLowQualityThumbnail && hasFailedLoadingHighQualityThumbnail && hasFailedLoadingFullQualityImage;
|
||||
}
|
||||
|
||||
private bool IsPng(IFileSystemItem item)
|
||||
{
|
||||
return item.Extension == ".png";
|
||||
}
|
||||
|
||||
private bool IsSvg(IFileSystemItem item)
|
||||
{
|
||||
return item.Extension == ".svg";
|
||||
}
|
||||
|
||||
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
|
||||
{
|
||||
// Image types
|
||||
".bmp",
|
||||
".gif",
|
||||
".jpg",
|
||||
".jfif",
|
||||
".jfi",
|
||||
".jif",
|
||||
".jpeg",
|
||||
".jpe",
|
||||
".png",
|
||||
".tif", // very slow for large files: no thumbnail?
|
||||
".tiff", // NEED TO TEST
|
||||
".dib", // NEED TO TEST
|
||||
".heic",
|
||||
".heif",
|
||||
".hif", // NEED TO TEST
|
||||
".avif", // NEED TO TEST
|
||||
".jxr",
|
||||
".wdp",
|
||||
".ico", // NEED TO TEST
|
||||
".thumb", // NEED TO TEST
|
||||
|
||||
// Raw types
|
||||
".arw",
|
||||
".cr2",
|
||||
".crw",
|
||||
".erf",
|
||||
".kdc", // NEED TO TEST
|
||||
".mrw",
|
||||
".nef",
|
||||
".nrw",
|
||||
".orf",
|
||||
".pef",
|
||||
".raf",
|
||||
".raw",
|
||||
".rw2",
|
||||
".rwl",
|
||||
".sr2",
|
||||
".srw",
|
||||
".srf",
|
||||
".dcs", // NEED TO TEST
|
||||
".dcr",
|
||||
".drf", // NEED TO TEST
|
||||
".k25",
|
||||
".3fr",
|
||||
".ari", // NEED TO TEST
|
||||
".bay", // NEED TO TEST
|
||||
".cap", // NEED TO TEST
|
||||
".iiq",
|
||||
".eip", // NEED TO TEST
|
||||
".fff",
|
||||
".mef",
|
||||
|
||||
// ".mdc", // Crashes in GetFullBitmapFromPathAsync
|
||||
".mos",
|
||||
".R3D",
|
||||
".rwz", // NEED TO TEST
|
||||
".x3f",
|
||||
".ori",
|
||||
".cr3",
|
||||
|
||||
".svg",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 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;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public interface IBrowserPreviewer : IPreviewer
|
||||
{
|
||||
public Uri? Preview { get; }
|
||||
|
||||
public bool IsDevFilePreview { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// 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.Media;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers.Interfaces
|
||||
{
|
||||
public interface IImagePreviewer : IPreviewer
|
||||
{
|
||||
public ImageSource? Preview { get; }
|
||||
|
||||
public double ScalingFactor { get; set; }
|
||||
|
||||
public Size MaxImageSize { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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.ComponentModel;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public interface IPreviewer : INotifyPropertyChanged
|
||||
{
|
||||
PreviewState State { get; set; }
|
||||
|
||||
public static bool IsFileTypeSupported(string fileExt) => throw new NotImplementedException();
|
||||
|
||||
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task LoadPreviewAsync(CancellationToken cancellationToken);
|
||||
|
||||
Task CopyAsync();
|
||||
}
|
||||
|
||||
public enum PreviewState
|
||||
{
|
||||
Uninitialized,
|
||||
Loading,
|
||||
Loaded,
|
||||
Error,
|
||||
}
|
||||
}
|
||||
@@ -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 Microsoft.UI.Xaml.Media;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public interface IUnsupportedFilePreviewer : IPreviewer
|
||||
{
|
||||
public ImageSource? IconPreview { get; }
|
||||
|
||||
public string? FileName { get; }
|
||||
|
||||
public string? FileType { get; }
|
||||
|
||||
public string? FileSize { get; }
|
||||
|
||||
public string? DateModified { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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.PowerToys.Telemetry;
|
||||
using Peek.Common.Models;
|
||||
using Peek.UI.Telemetry.Events;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public class PreviewerFactory
|
||||
{
|
||||
public IPreviewer Create(IFileSystemItem file)
|
||||
{
|
||||
if (ImagePreviewer.IsFileTypeSupported(file.Extension))
|
||||
{
|
||||
return new ImagePreviewer(file);
|
||||
}
|
||||
else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension))
|
||||
{
|
||||
return new WebBrowserPreviewer(file);
|
||||
}
|
||||
|
||||
// Other previewer types check their supported file types here
|
||||
return CreateDefaultPreviewer(file);
|
||||
}
|
||||
|
||||
public IPreviewer CreateDefaultPreviewer(IFileSystemItem file)
|
||||
{
|
||||
PowerToysTelemetry.Log.WriteEvent(new ErrorEvent() { Failure = ErrorEvent.FailureType.FileNotSupported });
|
||||
return new UnsupportedFilePreviewer(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// 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.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.Common.Models;
|
||||
using Peek.FilePreviewer.Previewers.Helpers;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public partial class UnsupportedFilePreviewer : ObservableObject, IUnsupportedFilePreviewer, IDisposable
|
||||
{
|
||||
[ObservableProperty]
|
||||
private ImageSource? iconPreview;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? fileName;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? fileType;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? fileSize;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? dateModified;
|
||||
|
||||
[ObservableProperty]
|
||||
private PreviewState state;
|
||||
|
||||
public UnsupportedFilePreviewer(IFileSystemItem file)
|
||||
{
|
||||
Item = file;
|
||||
FileName = file.Name;
|
||||
DateModified = file.DateModified.ToString(CultureInfo.CurrentCulture);
|
||||
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
public bool IsPreviewLoaded => iconPreview != null;
|
||||
|
||||
private IFileSystemItem Item { get; }
|
||||
|
||||
private DispatcherQueue Dispatcher { get; }
|
||||
|
||||
private Task<bool>? IconPreviewTask { get; set; }
|
||||
|
||||
private Task<bool>? DisplayInfoTask { get; set; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Size? size = new Size(680, 500);
|
||||
return Task.FromResult(size);
|
||||
}
|
||||
|
||||
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
State = PreviewState.Loading;
|
||||
|
||||
IconPreviewTask = LoadIconPreviewAsync(cancellationToken);
|
||||
DisplayInfoTask = LoadDisplayInfoAsync(cancellationToken);
|
||||
|
||||
await Task.WhenAll(IconPreviewTask, DisplayInfoTask);
|
||||
|
||||
if (HasFailedLoadingPreview())
|
||||
{
|
||||
State = PreviewState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CopyAsync()
|
||||
{
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
var storageItem = await Item.GetStorageItemAsync();
|
||||
ClipboardHelper.SaveToClipboard(storageItem);
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> LoadIconPreviewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TaskExtension.RunSafe(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var iconBitmap = await IconHelper.GetIconAsync(Path.GetFullPath(Item.Path), cancellationToken);
|
||||
IconPreview = iconBitmap;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TaskExtension.RunSafe(async () =>
|
||||
{
|
||||
// File Properties
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var bytes = await Task.Run(Item.GetSizeInBytes);
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var type = await Task.Run(Item.GetContentTypeAsync);
|
||||
|
||||
await Dispatcher.RunOnUiThread(() =>
|
||||
{
|
||||
FileSize = ReadableStringHelper.BytesToReadableString(bytes);
|
||||
FileType = type;
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
partial void OnIconPreviewChanged(ImageSource? value)
|
||||
{
|
||||
if (IconPreview != null)
|
||||
{
|
||||
State = PreviewState.Loaded;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasFailedLoadingPreview()
|
||||
{
|
||||
var hasFailedLoadingIconPreview = !(IconPreviewTask?.Result ?? true);
|
||||
var hasFailedLoadingDisplayInfo = !(DisplayInfoTask?.Result ?? true);
|
||||
|
||||
return hasFailedLoadingIconPreview && hasFailedLoadingDisplayInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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.IO;
|
||||
using Common.UI;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public class MarkdownHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Prepares temp html for the previewing
|
||||
/// </summary>
|
||||
public static string PreviewTempFile(string fileText, string filePath, string tempFolder)
|
||||
{
|
||||
string theme = ThemeManager.GetWindowsBaseColor().ToLowerInvariant();
|
||||
string markdownHTML = Microsoft.PowerToys.FilePreviewCommon.MarkdownHelper.MarkdownHtml(fileText, theme, filePath, ImageBlockedCallback);
|
||||
|
||||
string filename = tempFolder + "\\" + Guid.NewGuid().ToString() + ".html";
|
||||
File.WriteAllText(filename, markdownHTML);
|
||||
return filename;
|
||||
}
|
||||
|
||||
private static void ImageBlockedCallback()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.Text.Json;
|
||||
using Common.UI;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public class MonacoHelper
|
||||
{
|
||||
public static HashSet<string> GetExtensions()
|
||||
{
|
||||
HashSet<string> set = new HashSet<string>();
|
||||
try
|
||||
{
|
||||
JsonDocument languageListDocument = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguages();
|
||||
JsonElement languageList = languageListDocument.RootElement.GetProperty("list");
|
||||
foreach (JsonElement e in languageList.EnumerateArray())
|
||||
{
|
||||
for (int j = 0; j < e.GetProperty("extensions").GetArrayLength(); j++)
|
||||
{
|
||||
set.Add(e.GetProperty("extensions")[j].ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prepares temp html for the previewing
|
||||
/// </summary>
|
||||
public static string PreviewTempFile(string fileText, string extension, string tempFolder)
|
||||
{
|
||||
// TODO: check if file is too big, add MaxFileSize to settings
|
||||
return InitializeIndexFileAndSelectedFile(fileText, extension, tempFolder);
|
||||
}
|
||||
|
||||
private static string InitializeIndexFileAndSelectedFile(string fileContent, string extension, string tempFolder)
|
||||
{
|
||||
string vsCodeLangSet = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.GetLanguage(extension);
|
||||
string base64FileCode = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(fileContent));
|
||||
string theme = ThemeManager.GetWindowsBaseColor().ToLowerInvariant();
|
||||
|
||||
// prepping index html to load in
|
||||
string html = Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.ReadIndexHtml();
|
||||
|
||||
html = html.Replace("[[PT_LANG]]", vsCodeLangSet, StringComparison.InvariantCulture);
|
||||
html = html.Replace("[[PT_WRAP]]", "1", StringComparison.InvariantCulture); // TODO: add to settings
|
||||
html = html.Replace("[[PT_THEME]]", theme, StringComparison.InvariantCulture);
|
||||
html = html.Replace("[[PT_CODE]]", base64FileCode, StringComparison.InvariantCulture);
|
||||
html = html.Replace("[[PT_URL]]", Microsoft.PowerToys.FilePreviewCommon.MonacoHelper.VirtualHostName, StringComparison.InvariantCulture);
|
||||
|
||||
string filename = tempFolder + "\\" + Guid.NewGuid().ToString() + ".html";
|
||||
File.WriteAllText(filename, html);
|
||||
return filename;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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.IO;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public static class ReadHelper
|
||||
{
|
||||
public static async Task<string> Read(string path)
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
|
||||
using var sr = new StreamReader(fs, Encoding.UTF8);
|
||||
|
||||
string content = await sr.ReadToEndAsync();
|
||||
return content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Peek.Common.Constants;
|
||||
using Peek.Common.Extensions;
|
||||
using Peek.Common.Helpers;
|
||||
using Peek.Common.Models;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Peek.FilePreviewer.Previewers
|
||||
{
|
||||
public partial class WebBrowserPreviewer : ObservableObject, IBrowserPreviewer, IDisposable
|
||||
{
|
||||
private static readonly HashSet<string> _supportedFileTypes = new HashSet<string>
|
||||
{
|
||||
// Web
|
||||
".html",
|
||||
".htm",
|
||||
|
||||
// Document
|
||||
".pdf",
|
||||
|
||||
// Markdown
|
||||
".md",
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> _supportedMonacoFileTypes = MonacoHelper.GetExtensions();
|
||||
|
||||
[ObservableProperty]
|
||||
private Uri? preview;
|
||||
|
||||
[ObservableProperty]
|
||||
private PreviewState state;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool isDevFilePreview;
|
||||
|
||||
public WebBrowserPreviewer(IFileSystemItem file)
|
||||
{
|
||||
File = file;
|
||||
Dispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private IFileSystemItem File { get; }
|
||||
|
||||
public bool IsPreviewLoaded => preview != null;
|
||||
|
||||
private DispatcherQueue Dispatcher { get; }
|
||||
|
||||
private Task<bool>? DisplayInfoTask { get; set; }
|
||||
|
||||
public Task<Size?> GetPreviewSizeAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
Size? size = null;
|
||||
return Task.FromResult(size);
|
||||
}
|
||||
|
||||
public async Task LoadPreviewAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
State = PreviewState.Loading;
|
||||
await LoadDisplayInfoAsync(cancellationToken);
|
||||
|
||||
if (HasFailedLoadingPreview())
|
||||
{
|
||||
State = PreviewState.Error;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<bool> LoadDisplayInfoAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return TaskExtension.RunSafe(async () =>
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
bool isHtml = File.Extension == ".html";
|
||||
bool isMarkdown = File.Extension == ".md";
|
||||
IsDevFilePreview = _supportedMonacoFileTypes.Contains(File.Extension);
|
||||
|
||||
if (IsDevFilePreview && !isHtml && !isMarkdown)
|
||||
{
|
||||
var raw = await ReadHelper.Read(File.Path.ToString());
|
||||
Preview = new Uri(MonacoHelper.PreviewTempFile(raw, File.Extension, TempFolderPath.Path));
|
||||
}
|
||||
else if (isMarkdown)
|
||||
{
|
||||
var raw = await ReadHelper.Read(File.Path.ToString());
|
||||
Preview = new Uri(MarkdownHelper.PreviewTempFile(raw, File.Path, TempFolderPath.Path));
|
||||
}
|
||||
else
|
||||
{
|
||||
Preview = new Uri(File.Path);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public async Task CopyAsync()
|
||||
{
|
||||
await Dispatcher.RunOnUiThread(async () =>
|
||||
{
|
||||
var storageItem = await File.GetStorageItemAsync();
|
||||
ClipboardHelper.SaveToClipboard(storageItem);
|
||||
});
|
||||
}
|
||||
|
||||
public static bool IsFileTypeSupported(string fileExt)
|
||||
{
|
||||
return _supportedFileTypes.Contains(fileExt) || _supportedMonacoFileTypes.Contains(fileExt);
|
||||
}
|
||||
|
||||
private bool HasFailedLoadingPreview()
|
||||
{
|
||||
return !(DisplayInfoTask?.Result ?? true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user