From 1024760cef6db5a83ed3adc00ba2bc653b0db072 Mon Sep 17 00:00:00 2001 From: d2dyno <53011783+d2dyno1@users.noreply.github.com> Date: Mon, 6 Jan 2025 03:50:22 +0100 Subject: [PATCH] Code Quality: Added design and basic functionality for the upcoming Shelf feature (#16673) --- .../Data/Contracts/ImagingService.cs | 7 +- src/Files.App/Data/Items/ShelfItem.cs | 40 +++++++++ src/Files.App/Strings/en-US/Resources.resw | 14 +++ .../UserControls/Pane/ShelfPane.xaml | 87 +++++++++++++++++- .../UserControls/Pane/ShelfPane.xaml.cs | 88 +++++++++++++++++++ src/Files.App/Views/MainPage.xaml | 1 + src/Files.Shared/Utils/IAsyncInitialize.cs | 5 +- src/Files.Shared/Utils/IWrapper.cs | 14 +++ 8 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 src/Files.App/Data/Items/ShelfItem.cs create mode 100644 src/Files.Shared/Utils/IWrapper.cs diff --git a/src/Files.App/Data/Contracts/ImagingService.cs b/src/Files.App/Data/Contracts/ImagingService.cs index 390824ac9d93..30d3dcd0a427 100644 --- a/src/Files.App/Data/Contracts/ImagingService.cs +++ b/src/Files.App/Data/Contracts/ImagingService.cs @@ -13,15 +13,12 @@ internal sealed class ImagingService : IImageService /// <inheritdoc/> public async Task<IImage?> GetIconAsync(IStorable storable, CancellationToken cancellationToken) { - if (storable is not ILocatableStorable locatableStorable) - return null; - - var iconData = await FileThumbnailHelper.LoadIconFromPathAsync(locatableStorable.Path, 24u, ThumbnailMode.ListView, ThumbnailOptions.ResizeThumbnail); + var iconData = await FileThumbnailHelper.GetIconAsync(storable.Id, Constants.ShellIconSizes.Small, storable is IFolder, IconOptions.ReturnIconOnly | IconOptions.UseCurrentScale); if (iconData is null) return null; var bitmapImage = await iconData.ToBitmapAsync(); - return new BitmapImageModel(bitmapImage); + return bitmapImage is null ? null : new BitmapImageModel(bitmapImage); } public async Task<IImage?> GetImageModelFromDataAsync(byte[] rawData) diff --git a/src/Files.App/Data/Items/ShelfItem.cs b/src/Files.App/Data/Items/ShelfItem.cs new file mode 100644 index 000000000000..7facfabce5b9 --- /dev/null +++ b/src/Files.App/Data/Items/ShelfItem.cs @@ -0,0 +1,40 @@ +using Files.Shared.Utils; + +namespace Files.App.Data.Items +{ + [Bindable(true)] + public sealed partial class ShelfItem : ObservableObject, IWrapper<IStorable>, IAsyncInitialize + { + private readonly IImageService _imageService; + private readonly ICollection<ShelfItem> _sourceCollection; + + [ObservableProperty] private IImage? _Icon; + [ObservableProperty] private string? _Name; + [ObservableProperty] private string? _Path; + + /// <inheritdoc/> + public IStorable Inner { get; } + + public ShelfItem(IStorable storable, ICollection<ShelfItem> sourceCollection, IImage? icon = null) + { + _imageService = Ioc.Default.GetRequiredService<IImageService>(); + _sourceCollection = sourceCollection; + Inner = storable; + Icon = icon; + Name = storable.Name; + Path = storable.Id; + } + + /// <inheritdoc/> + public async Task InitAsync(CancellationToken cancellationToken = default) + { + Icon = await _imageService.GetIconAsync(Inner, cancellationToken); + } + + [RelayCommand] + private void Remove() + { + _sourceCollection.Remove(this); + } + } +} diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 02dbe67ff39d..a219e91f76cc 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -4022,4 +4022,18 @@ <data name="ShowShelfPane" xml:space="preserve"> <value>Show Shelf Pane</value> </data> + <data name="Shelf" xml:space="preserve"> + <value>Shelf</value> + <comment>'Shelf' refers to the Shelf Pane feature, where users can conveniently drag and drop files for quick access and perform bulk actions with ease.</comment> + </data> + <data name="ClearItems" xml:space="preserve"> + <value>Clear items</value> + </data> + <data name="RemoveFromShelf" xml:space="preserve"> + <value>Remove from shelf</value> + </data> + <data name="AddToShelf" xml:space="preserve"> + <value>Add to Shelf</value> + <comment>Tooltip that displays when dragging items to the Shelf Pane</comment> + </data> </root> \ No newline at end of file diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml b/src/Files.App/UserControls/Pane/ShelfPane.xaml index 1804873e84d6..952b0ca4861b 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml @@ -3,20 +3,101 @@ x:Class="Files.App.UserControls.ShelfPane" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:controls="using:Files.App.Controls" xmlns:converters="using:Files.App.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:data="using:Files.App.Data.Items" xmlns:helpers="using:Files.App.Helpers" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:usercontrols="using:Files.App.UserControls" mc:Ignorable="d"> + <UserControl.Resources> + <converters:ImageModelToImageConverter x:Key="ImageModelToImageConverter" /> + </UserControl.Resources> + <Grid Width="240" + Padding="12" + AllowDrop="True" Background="{ThemeResource App.Theme.InfoPane.BackgroundBrush}" BackgroundSizing="InnerBorderEdge" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1" - CornerRadius="8" /> + CornerRadius="8" + DragOver="Shelf_DragOver" + Drop="Shelf_Drop" + RowSpacing="8"> + + <Grid.RowDefinitions> + <RowDefinition Height="Auto" /> + <RowDefinition Height="*" /> + <RowDefinition Height="Auto" /> + </Grid.RowDefinitions> + + <StackPanel Grid.Row="0" Spacing="8"> + + <!-- Title --> + <TextBlock + HorizontalAlignment="Center" + Foreground="{ThemeResource TextFillColorTertiaryBrush}" + Style="{StaticResource App.Theme.BodyTextBlockStyle}" + Text="{helpers:ResourceString Name=Shelf}" /> + + <!-- (Divider) --> + <Border Height="1" Background="{ThemeResource DividerStrokeColorDefaultBrush}" /> + + </StackPanel> + + <!-- Items List --> + <ListView + Grid.Row="1" + DragItemsStarting="ListView_DragItemsStarting" + ItemsSource="{x:Bind ItemsSource, Mode=OneWay}" + ScrollViewer.VerticalScrollBarVisibility="Auto" + ScrollViewer.VerticalScrollMode="Auto" + SelectionMode="Extended"> + <ListView.ItemTemplate> + <DataTemplate x:DataType="data:ShelfItem"> + <StackPanel Orientation="Horizontal" Spacing="8"> + <Image + Width="16" + Height="16" + Source="{x:Bind Icon, Mode=OneWay, Converter={StaticResource ImageModelToImageConverter}}" /> + <TextBlock Text="{x:Bind Name, Mode=OneWay}" /> + + <StackPanel.ContextFlyout> + <MenuFlyout> + <MenuFlyoutItem Command="{x:Bind RemoveCommand}" Text="{helpers:ResourceString Name=RemoveFromShelf}"> + <MenuFlyoutItem.Icon> + <FontIcon Glyph="" /> + </MenuFlyoutItem.Icon> + </MenuFlyoutItem> + </MenuFlyout> + </StackPanel.ContextFlyout> + </StackPanel> + </DataTemplate> + </ListView.ItemTemplate> + + <ListView.ItemContainerStyle> + <Style BasedOn="{StaticResource DefaultListViewItemStyle}" TargetType="ListViewItem"> + <Setter Property="Margin" Value="-4,0,-4,0" /> + <Setter Property="MinHeight" Value="36" /> + </Style> + </ListView.ItemContainerStyle> + </ListView> + + + <StackPanel Grid.Row="2" Spacing="4"> + + <!-- (Divider) --> + <Border Height="1" Background="{ThemeResource DividerStrokeColorDefaultBrush}" /> + + <!-- Bottom Actions --> + <HyperlinkButton + HorizontalAlignment="Center" + VerticalAlignment="Center" + Command="{x:Bind ClearCommand, Mode=OneWay}" + Content="{helpers:ResourceString Name=ClearItems}" /> + + </StackPanel> + </Grid> </UserControl> diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs index 25946f599056..baf685530c70 100644 --- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs +++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs @@ -1,7 +1,13 @@ // Copyright (c) Files Community // Licensed under the MIT License. +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using System.Runtime.InteropServices.ComTypes; +using System.Windows.Input; +using Vanara.PInvoke; +using Windows.ApplicationModel.DataTransfer; +using WinRT; namespace Files.App.UserControls { @@ -9,7 +15,89 @@ public sealed partial class ShelfPane : UserControl { public ShelfPane() { + // TODO: [Shelf] Remove once view model is connected + ItemsSource = new ObservableCollection<ShelfItem>(); + InitializeComponent(); } + + private void Shelf_DragOver(object sender, DragEventArgs e) + { + if (!FilesystemHelpers.HasDraggedStorageItems(e.DataView)) + return; + + e.Handled = true; + e.DragUIOverride.Caption = Strings.AddToShelf.GetLocalizedResource(); + e.AcceptedOperation = DataPackageOperation.Link; + } + + private async void Shelf_Drop(object sender, DragEventArgs e) + { + if (ItemsSource is null) + return; + + // Get items + var storageService = Ioc.Default.GetRequiredService<IStorageService>(); + var storageItems = (await FilesystemHelpers.GetDraggedStorageItems(e.DataView)).ToArray(); + + // Add to list + foreach (var item in storageItems) + { + var storable = item switch + { + StorageFileWithPath => (IStorable?)await storageService.TryGetFileAsync(item.Path), + StorageFolderWithPath => (IStorable?)await storageService.TryGetFolderAsync(item.Path), + _ => null + }; + + if (storable is null) + continue; + + var shelfItem = new ShelfItem(storable, ItemsSource); + _ = shelfItem.InitAsync(); + + ItemsSource.Add(shelfItem); + } + } + + private void ListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e) + { + if (ItemsSource is null) + return; + + var shellItemList = SafetyExtensions.IgnoreExceptions(() => ItemsSource.Select(x => new Vanara.Windows.Shell.ShellItem(x.Inner.Id)).ToArray()); + if (shellItemList?[0].FileSystemPath is not null) + { + var iddo = shellItemList[0].Parent?.GetChildrenUIObjects<IDataObject>(HWND.NULL, shellItemList); + if (iddo is null) + return; + + shellItemList.ForEach(x => x.Dispose()); + var dataObjectProvider = e.Data.As<Shell32.IDataObjectProvider>(); + dataObjectProvider.SetDataObject(iddo); + } + else + { + // Only support IStorageItem capable paths + var storageItems = ItemsSource.Select(x => VirtualStorageItem.FromPath(x.Inner.Id)); + e.Data.SetStorageItems(storageItems, false); + } + } + + public IList<ShelfItem>? ItemsSource + { + get => (IList<ShelfItem>?)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + public static readonly DependencyProperty ItemsSourceProperty = + DependencyProperty.Register(nameof(ItemsSource), typeof(IList<ShelfItem>), typeof(ShelfPane), new PropertyMetadata(null)); + + public ICommand? ClearCommand + { + get => (ICommand?)GetValue(ClearCommandProperty); + set => SetValue(ClearCommandProperty, value); + } + public static readonly DependencyProperty ClearCommandProperty = + DependencyProperty.Register(nameof(ClearCommand), typeof(ICommand), typeof(ShelfPane), new PropertyMetadata(null)); } } diff --git a/src/Files.App/Views/MainPage.xaml b/src/Files.App/Views/MainPage.xaml index 0c9da874c568..74aec3c6d77c 100644 --- a/src/Files.App/Views/MainPage.xaml +++ b/src/Files.App/Views/MainPage.xaml @@ -248,6 +248,7 @@ ShowInfoText="{x:Bind SidebarAdaptiveViewModel.PaneHolder.ActivePaneOrColumn.InstanceViewModel.IsPageTypeNotHome, Mode=OneWay}" Visibility="{x:Bind SidebarAdaptiveViewModel.PaneHolder.ActivePaneOrColumn.InstanceViewModel.IsPageTypeNotHome, Mode=OneWay}" /> + <!-- Shelf Pane --> <uc:ShelfPane x:Name="ShelfPane" Grid.Row="0" diff --git a/src/Files.Shared/Utils/IAsyncInitialize.cs b/src/Files.Shared/Utils/IAsyncInitialize.cs index 8591c225a96f..86bcfd7a8795 100644 --- a/src/Files.Shared/Utils/IAsyncInitialize.cs +++ b/src/Files.Shared/Utils/IAsyncInitialize.cs @@ -1,7 +1,4 @@ -// Copyright (c) Files Community -// Licensed under the MIT License. - -using System.Threading; +using System.Threading; using System.Threading.Tasks; namespace Files.Shared.Utils diff --git a/src/Files.Shared/Utils/IWrapper.cs b/src/Files.Shared/Utils/IWrapper.cs new file mode 100644 index 000000000000..cb9b39701d87 --- /dev/null +++ b/src/Files.Shared/Utils/IWrapper.cs @@ -0,0 +1,14 @@ +namespace Files.Shared.Utils +{ + /// <summary> + /// Wraps and exposes <typeparamref name="T"/> implementation for access. + /// </summary> + /// <typeparam name="T">The wrapped type.</typeparam> + public interface IWrapper<out T> + { + /// <summary> + /// Gets the inner member wrapped by the implementation. + /// </summary> + T Inner { get; } + } +}