Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/Files.App/Data/Contracts/ImagingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
40 changes: 40 additions & 0 deletions src/Files.App/Data/Items/ShelfItem.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
14 changes: 14 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -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>
87 changes: 84 additions & 3 deletions src/Files.App/UserControls/Pane/ShelfPane.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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="&#xE738;" />
</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>
88 changes: 88 additions & 0 deletions src/Files.App/UserControls/Pane/ShelfPane.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,103 @@
// 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
{
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));
}
}
1 change: 1 addition & 0 deletions src/Files.App/Views/MainPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
5 changes: 1 addition & 4 deletions src/Files.Shared/Utils/IAsyncInitialize.cs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 14 additions & 0 deletions src/Files.Shared/Utils/IWrapper.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
}