diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 7a3aa7d347..ffc4aec16f 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -38,6 +38,7 @@ ALPHATYPE AModifier AMPROPERTY AMPROPSETID +amr ANDSCANS animatedvisuals ansicolor @@ -488,6 +489,7 @@ Filterkeyboard Filterx findfast FIXEDFILEINFO +flac flyouts FOF FOFX diff --git a/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs b/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs index 2a7a50caa5..a3b6c9b6b9 100644 --- a/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs +++ b/src/modules/peek/Peek.Common/Models/Win32/PropertyKey.cs @@ -63,5 +63,9 @@ namespace Peek.Common.Models public static readonly PropertyKey FileType = new PropertyKey(new Guid(0xb725f130, 0x47ef, 0x101a, 0xa5, 0xf1, 0x02, 0x60, 0x8c, 0x9e, 0xeb, 0xac), 4); public static readonly PropertyKey FrameWidth = new PropertyKey(new Guid(0x64440491, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 3); public static readonly PropertyKey FrameHeight = new PropertyKey(new Guid(0x64440491, 0x4C8B, 0x11D1, 0x8B, 0x70, 0x08, 0x00, 0x36, 0xB1, 0x1A, 0x03), 4); + public static readonly PropertyKey MusicTitle = new PropertyKey(new Guid(0xf29f85e0, 0x4ff9, 0x1068, 0xab, 0x91, 0x08, 0x00, 0x2b, 0x27, 0xb3, 0xd9), 2); + public static readonly PropertyKey MusicDisplayArtist = new PropertyKey(new Guid(0xFD122953, 0xFA93, 0x4EF7, 0x92, 0xC3, 0x04, 0xC9, 0x46, 0xB2, 0xF7, 0xC8), 100); + public static readonly PropertyKey MusicAlbum = new PropertyKey(new Guid(0x56a3372e, 0xce9c, 0x11d2, 0x9f, 0xe, 0x0, 0x60, 0x97, 0xc6, 0x86, 0xf6), 4); + public static readonly PropertyKey MusicDuration = new PropertyKey(new Guid(0x64440490, 0x4c8b, 0x11d1, 0x8b, 0x70, 0x8, 0x0, 0x36, 0xb1, 0x1a, 0x3), 3); } } diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml new file mode 100644 index 0000000000..3ed9e163b3 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml.cs b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml.cs new file mode 100644 index 0000000000..c52f701c92 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Controls/AudioControl.xaml.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Peek.FilePreviewer.Previewers.MediaPreviewer.Models; + +namespace Peek.FilePreviewer.Controls +{ + public sealed partial class AudioControl : UserControl + { + public static readonly DependencyProperty SourceProperty = DependencyProperty.Register( + nameof(Source), + typeof(AudioPreviewData), + typeof(AudioControl), + new PropertyMetadata(null, new PropertyChangedCallback((d, e) => ((AudioControl)d).SourcePropertyChanged()))); + + public static readonly DependencyProperty ToolTipTextProperty = DependencyProperty.Register( + nameof(ToolTipText), + typeof(string), + typeof(AudioControl), + new PropertyMetadata(null)); + + public AudioPreviewData? Source + { + get { return (AudioPreviewData)GetValue(SourceProperty); } + set { SetValue(SourceProperty, value); } + } + + public string ToolTipText + { + get { return (string)GetValue(ToolTipTextProperty); } + set { SetValue(ToolTipTextProperty, value); } + } + + public AudioControl() + { + this.InitializeComponent(); + } + + private void SourcePropertyChanged() + { + if (Source == null) + { + PlayerElement.MediaPlayer.Pause(); + PlayerElement.MediaPlayer.Source = null; + } + } + + private void KeyboardAccelerator_Space_Invoked(Microsoft.UI.Xaml.Input.KeyboardAccelerator sender, Microsoft.UI.Xaml.Input.KeyboardAcceleratorInvokedEventArgs args) + { + var mediaPlayer = PlayerElement.MediaPlayer; + + if (mediaPlayer.Source == null || !mediaPlayer.CanPause) + { + return; + } + + if (mediaPlayer.CurrentState == Windows.Media.Playback.MediaPlayerState.Playing) + { + mediaPlayer.Pause(); + } + else + { + mediaPlayer.Play(); + } + + // Prevent the keyboard accelerator to be called twice + args.Handled = true; + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml b/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml index 59bd179d0a..9861147fff 100644 --- a/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml +++ b/src/modules/peek/Peek.FilePreviewer/Controls/DriveControl.xaml @@ -10,6 +10,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" SizeChanged="SizeChanged_Handler" mc:Ignorable="d"> + @@ -52,6 +52,12 @@ + + Previewer as IVideoPreviewer; + public IAudioPreviewer? AudioPreviewer => Previewer as IAudioPreviewer; + public IBrowserPreviewer? BrowserPreviewer => Previewer as IBrowserPreviewer; public IArchivePreviewer? ArchivePreviewer => Previewer as IArchivePreviewer; @@ -152,6 +155,8 @@ namespace Peek.FilePreviewer Previewer = null; ImagePreview.Visibility = Visibility.Collapsed; VideoPreview.Visibility = Visibility.Collapsed; + + AudioPreview.Visibility = Visibility.Collapsed; BrowserPreview.Visibility = Visibility.Collapsed; ArchivePreview.Visibility = Visibility.Collapsed; DrivePreview.Visibility = Visibility.Collapsed; @@ -159,6 +164,8 @@ namespace Peek.FilePreviewer ImagePreview.FlowDirection = FlowDirection.LeftToRight; VideoPreview.FlowDirection = FlowDirection.LeftToRight; + + AudioPreview.FlowDirection = FlowDirection.LeftToRight; BrowserPreview.FlowDirection = FlowDirection.LeftToRight; ArchivePreview.FlowDirection = FlowDirection.LeftToRight; DrivePreview.FlowDirection = FlowDirection.LeftToRight; @@ -203,7 +210,7 @@ namespace Peek.FilePreviewer await Previewer.LoadPreviewAsync(cancellationToken); cancellationToken.ThrowIfCancellationRequested(); - await UpdateImageTooltipAsync(cancellationToken); + await UpdateTooltipAsync(cancellationToken); } catch (OperationCanceledException) { @@ -225,6 +232,7 @@ namespace Peek.FilePreviewer VideoPreview.MediaPlayer.Source = null; VideoPreview.Source = null; + AudioPreview.Source = null; ImagePreview.Source = null; ArchivePreview.Source = null; BrowserPreview.Source = null; @@ -327,7 +335,7 @@ namespace Peek.FilePreviewer args.Handled = true; } - private async Task UpdateImageTooltipAsync(CancellationToken cancellationToken) + private async Task UpdateTooltipAsync(CancellationToken cancellationToken) { if (Item == null) { @@ -353,7 +361,7 @@ namespace Peek.FilePreviewer string fileSizeFormatted = string.IsNullOrEmpty(fileSize) ? string.Empty : "\n" + ReadableStringHelper.FormatResourceString("PreviewTooltip_FileSize", fileSize); sb.Append(fileSizeFormatted); - ImageInfoTooltip = sb.ToString(); + InfoTooltip = sb.ToString(); } } } diff --git a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj index 8c411077fd..8ffcb6cc8a 100644 --- a/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj +++ b/src/modules/peek/Peek.FilePreviewer/Peek.FilePreviewer.csproj @@ -17,6 +17,7 @@ + @@ -28,6 +29,7 @@ + @@ -47,6 +49,12 @@ + + + MSBuild:Compile + + + MSBuild:Compile diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IAudioPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IAudioPreviewer.cs new file mode 100644 index 0000000000..8e728dd321 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/Interfaces/IAudioPreviewer.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Peek.FilePreviewer.Previewers.MediaPreviewer.Models; + +namespace Peek.FilePreviewer.Previewers.Interfaces +{ + public interface IAudioPreviewer : IPreviewer + { + public AudioPreviewData? Preview { get; } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs new file mode 100644 index 0000000000..696efe906a --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/AudioPreviewer.cs @@ -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 System.Collections.Generic; +using System.Globalization; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Dispatching; +using Microsoft.UI.Xaml.Media.Imaging; +using Peek.Common.Extensions; +using Peek.Common.Helpers; +using Peek.Common.Models; +using Peek.FilePreviewer.Models; +using Peek.FilePreviewer.Previewers.Helpers; +using Peek.FilePreviewer.Previewers.Interfaces; +using Peek.FilePreviewer.Previewers.MediaPreviewer.Models; +using Windows.Foundation; +using Windows.Media.Core; +using Windows.Storage; + +namespace Peek.FilePreviewer.Previewers.MediaPreviewer +{ + public partial class AudioPreviewer : ObservableObject, IAudioPreviewer + { + [ObservableProperty] + private PreviewState _state; + + [ObservableProperty] + private AudioPreviewData _preview; + + private IFileSystemItem Item { get; } + + private DispatcherQueue Dispatcher { get; } + + public AudioPreviewer(IFileSystemItem file) + { + Item = file; + Dispatcher = DispatcherQueue.GetForCurrentThread(); + Preview = new AudioPreviewData(); + } + + public async Task CopyAsync() + { + await Dispatcher.RunOnUiThread(async () => + { + var storageItem = await Item.GetStorageItemAsync(); + ClipboardHelper.SaveToClipboard(storageItem); + }); + } + + public Task GetPreviewSizeAsync(CancellationToken cancellationToken) + { + var size = new Size(680, 400); + var previewSize = new PreviewSize { MonitorSize = size, UseEffectivePixels = true }; + return Task.FromResult(previewSize); + } + + public async Task LoadPreviewAsync(CancellationToken cancellationToken) + { + State = PreviewState.Loading; + + var thumbnailTask = LoadThumbnailAsync(cancellationToken); + var sourceTask = LoadSourceAsync(cancellationToken); + var metadataTask = LoadMetadataAsync(cancellationToken); + + await Task.WhenAll(thumbnailTask, sourceTask, metadataTask); + + if (!thumbnailTask.Result || !sourceTask.Result || !metadataTask.Result) + { + State = PreviewState.Error; + } + else + { + State = PreviewState.Loaded; + } + } + + public Task LoadThumbnailAsync(CancellationToken cancellationToken) + { + return TaskExtension.RunSafe(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + await Dispatcher.RunOnUiThread(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + var thumbnail = await IconHelper.GetThumbnailAsync(Item.Path, cancellationToken) + ?? await IconHelper.GetIconAsync(Item.Path, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + + Preview.Thumbnail = thumbnail ?? new SvgImageSource(new Uri("ms-appx:///Assets/Peek/DefaultFileIcon.svg")); + }); + }); + } + + private Task LoadSourceAsync(CancellationToken cancellationToken) + { + return TaskExtension.RunSafe(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + var storageFile = await Item.GetStorageItemAsync() as StorageFile; + + await Dispatcher.RunOnUiThread(() => + { + cancellationToken.ThrowIfCancellationRequested(); + + Preview.MediaSource = MediaSource.CreateFromStorageFile(storageFile); + }); + }); + } + + private Task LoadMetadataAsync(CancellationToken cancellationToken) + { + return TaskExtension.RunSafe(async () => + { + cancellationToken.ThrowIfCancellationRequested(); + + await Dispatcher.RunOnUiThread(() => + { + cancellationToken.ThrowIfCancellationRequested(); + Preview.Title = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicTitle) + ?? Item.Name[..^Item.Extension.Length]; + + cancellationToken.ThrowIfCancellationRequested(); + var artist = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicDisplayArtist); + Preview.Artist = artist != null + ? string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("Audio_Artist"), artist) + : string.Empty; + + cancellationToken.ThrowIfCancellationRequested(); + var album = PropertyStoreHelper.TryGetStringProperty(Item.Path, PropertyKey.MusicAlbum); + Preview.Album = album != null + ? string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("Audio_Album"), album) + : string.Empty; + + cancellationToken.ThrowIfCancellationRequested(); + var ticksLength = PropertyStoreHelper.TryGetUlongProperty(Item.Path, PropertyKey.MusicDuration); + if (ticksLength.HasValue) + { + var length = TimeSpan.FromTicks((long)ticksLength); + var truncatedLength = new TimeSpan(length.Hours, length.Minutes, length.Seconds).ToString("g", CultureInfo.CurrentCulture); + Preview.Length = string.Format(CultureInfo.CurrentCulture, ResourceLoaderInstance.ResourceLoader.GetString("Audio_Length"), truncatedLength); + } + else + { + Preview.Length = string.Empty; + } + }); + }); + } + + public static bool IsFileTypeSupported(string fileExt) + { + return _supportedFileTypes.Contains(fileExt); + } + + private static readonly HashSet _supportedFileTypes = new() + { + ".aac", + ".ac3", + ".amr", + ".flac", + ".m4a", + ".mp3", + ".ogg", + ".wav", + ".wma", + }; + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Models/AudioPreviewData.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Models/AudioPreviewData.cs new file mode 100644 index 0000000000..d680dd3da4 --- /dev/null +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/MediaPreviewer/Models/AudioPreviewData.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation +// The Microsoft Corporation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.UI.Xaml.Media; +using Windows.Media.Core; + +namespace Peek.FilePreviewer.Previewers.MediaPreviewer.Models +{ + public partial class AudioPreviewData : ObservableObject + { + [ObservableProperty] + private MediaSource? _mediaSource; + + [ObservableProperty] + private ImageSource? _thumbnail; + + [ObservableProperty] + private string _title; + + [ObservableProperty] + private string _artist; + + [ObservableProperty] + private string _album; + + [ObservableProperty] + private string _length; + + public AudioPreviewData() + { + Artist = string.Empty; + Title = string.Empty; + Album = string.Empty; + Length = string.Empty; + } + } +} diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs index 089b96bff5..23f54e846d 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/PreviewerFactory.cs @@ -9,6 +9,7 @@ using Peek.Common.Models; using Peek.FilePreviewer.Models; using Peek.FilePreviewer.Previewers.Archives; using Peek.FilePreviewer.Previewers.Drive; +using Peek.FilePreviewer.Previewers.MediaPreviewer; using Peek.UI.Telemetry.Events; namespace Peek.FilePreviewer.Previewers @@ -32,6 +33,10 @@ namespace Peek.FilePreviewer.Previewers { return new VideoPreviewer(file); } + else if (AudioPreviewer.IsFileTypeSupported(file.Extension)) + { + return new AudioPreviewer(file); + } else if (WebBrowserPreviewer.IsFileTypeSupported(file.Extension)) { return new WebBrowserPreviewer(file, _previewSettings); diff --git a/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs index b4d9786697..50f7da2cc9 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/Views/TitleBar.xaml.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation +// 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. diff --git a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw index dba48568a3..9334638508 100644 --- a/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw +++ b/src/modules/peek/Peek.UI/Strings/en-us/Resources.resw @@ -306,4 +306,16 @@ Unknown Used for unknown drive type or file system + + Album: {0} + {0} is the title of the album read from file metadata + + + Artist: {0} + {0} is the artist read from file metadata + + + Length: {0} + {0} is the duration of the audio read from file metadata + \ No newline at end of file