[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:
Samuel Chapleau
2023-05-10 10:43:03 -07:00
committed by GitHub
parent 7ae93a9c7f
commit 648f30d1ab
361 changed files with 10709 additions and 182 deletions

View File

@@ -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);
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
});
}
}
}

View File

@@ -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",
};
}
}

View File

@@ -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; }
}
}

View File

@@ -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; }
}
}

View File

@@ -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,
}
}

View File

@@ -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; }
}
}

View File

@@ -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);
}
}
}

View 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;
}
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}