From 90994d2428caf6f2f89291bfcccb5597a82dc986 Mon Sep 17 00:00:00 2001
From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com>
Date: Fri, 25 Jul 2025 00:42:13 +0200
Subject: [PATCH 1/6] Re-enable shelf action feature
---
src/Files.App/Actions/Show/ToggleShelfPaneAction.cs | 8 --------
src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs | 4 +++-
2 files changed, 3 insertions(+), 9 deletions(-)
diff --git a/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs b/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs
index 43ff818fa7fa..0a4a1e6a5706 100644
--- a/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs
+++ b/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs
@@ -15,14 +15,6 @@ public string Description
public RichGlyph Glyph
=> new(themedIconStyle: "App.ThemedIcons.Shelf");
-
- // TODO Remove IsAccessibleGlobally when shelf feature is ready
- public bool IsAccessibleGlobally
- => AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev;
-
- // TODO Remove IsExecutable when shelf feature is ready
- public bool IsExecutable
- => AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev;
public bool IsOn
=> generalSettingsService.ShowShelfPane;
diff --git a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
index c4fa685b7bae..443b3811556a 100644
--- a/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
+++ b/src/Files.App/ViewModels/Layouts/BaseLayoutViewModel.cs
@@ -227,7 +227,9 @@ public async Task DropAsync(DragEventArgs e)
new MenuFlyoutItem() { Text = string.Format(Strings.CopyToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct =>
await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Copy, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) },
new MenuFlyoutItem() { Text = string.Format(Strings.MoveToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct =>
- await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Move, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) }
+ await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Move, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) },
+ new MenuFlyoutItem() { Text = string.Format(Strings.LinkToFolderCaptionText.GetLocalizedResource(), folderName), Command = new AsyncRelayCommand(async ct =>
+ await _associatedInstance.FilesystemHelpers.PerformOperationTypeAsync(DataPackageOperation.Link, e.DataView, _associatedInstance.ShellViewModel.WorkingDirectory, false, true)) }
}
};
From edfe5eca7032ea94b3993bb967d527803d5258aa Mon Sep 17 00:00:00 2001
From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com>
Date: Fri, 25 Jul 2025 01:15:43 +0200
Subject: [PATCH 2/6] Added a delete bulk action
---
.../UserControls/Pane/ShelfPane.xaml | 14 ++++++++++
.../UserControls/Pane/ShelfPane.xaml.cs | 11 ++++++--
.../ViewModels/UserControls/ShelfViewModel.cs | 28 +++++++++++++++++--
src/Files.App/Views/MainPage.xaml | 3 +-
4 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml b/src/Files.App/UserControls/Pane/ShelfPane.xaml
index 121268eeaf12..bd55bb5393bc 100644
--- a/src/Files.App/UserControls/Pane/ShelfPane.xaml
+++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml
@@ -3,6 +3,7 @@
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"
@@ -152,6 +153,19 @@
+
+
+
+
(ICommand?)GetValue(BulkDeleteCommandProperty);
+ set => SetValue(BulkDeleteCommandProperty, value);
+ }
+ public static readonly DependencyProperty BulkDeleteCommandProperty =
+ DependencyProperty.Register(nameof(BulkDeleteCommand), typeof(ICommand), typeof(ShelfPane), new PropertyMetadata(null));
+
public ICommand? ItemFocusedCommand
{
get => (ICommand?)GetValue(ItemFocusedCommandProperty);
@@ -135,6 +143,5 @@ public ICommand? ItemFocusedCommand
}
public static readonly DependencyProperty ItemFocusedCommandProperty =
DependencyProperty.Register(nameof(ItemFocusedCommand), typeof(ICommand), typeof(ShelfPane), new PropertyMetadata(null));
-
}
}
diff --git a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
index f5fdc8f62a10..ddb778bdd047 100644
--- a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
@@ -1,8 +1,8 @@
// Copyright (c) Files Community
// Licensed under the MIT License.
-using System.Collections.Specialized;
using Files.Shared.Utils;
+using System.Collections.Specialized;
namespace Files.App.ViewModels.UserControls
{
@@ -33,6 +33,28 @@ private void ClearItems()
Items.Clear();
}
+ [RelayCommand]
+ private async Task BulkDeleteAsync()
+ {
+ if (Items.IsEmpty())
+ return;
+
+ var context = Ioc.Default.GetRequiredService();
+ if (context.ShellPage is not { } shellPage)
+ return;
+
+ var itemsToDelete = Items.Select(x => StorageHelpers.FromPathAndType(x.Inner.Id, x.Inner switch
+ {
+ IFile => FilesystemItemType.File,
+ IFolder => FilesystemItemType.Directory,
+ _ => throw new ArgumentOutOfRangeException(nameof(ShelfViewModel))
+ }));
+
+ var settings = Ioc.Default.GetRequiredService();
+ await shellPage.FilesystemHelpers.DeleteItemsAsync(itemsToDelete, settings.DeleteConfirmationPolicy, false, true);
+ await shellPage.ShellViewModel.ApplyFilesAndFoldersChangesAsync();
+ }
+
private async void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@@ -46,7 +68,7 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
if (_watchers.TryGetValue(parentPath, out var reference))
{
// Only increase the reference count if the watcher already exists
- reference.Item2++;
+ reference.Item2 += 1;
return;
}
@@ -71,7 +93,7 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
return;
// Decrease the reference count and remove the watcher if no references are present
- reference.Item2--;
+ reference.Item2 -= 1;
if (reference.Item2 < 1)
{
reference.Item1.CollectionChanged -= Watcher_CollectionChanged;
diff --git a/src/Files.App/Views/MainPage.xaml b/src/Files.App/Views/MainPage.xaml
index 73587546e8e5..7e1e86801cbd 100644
--- a/src/Files.App/Views/MainPage.xaml
+++ b/src/Files.App/Views/MainPage.xaml
@@ -271,8 +271,9 @@
Grid.Column="3"
Margin="4,0,0,8"
x:Load="{x:Bind ViewModel.ShowShelfPane, Mode=OneWay}"
+ BulkDeleteCommand="{x:Bind ViewModel.ShelfViewModel.BulkDeleteCommand}"
ClearCommand="{x:Bind ViewModel.ShelfViewModel.ClearItemsCommand}"
- ItemFocusedCommand="{x:Bind Commands.ClearSelection, Mode=OneWay}"
+ ItemFocusedCommand="{x:Bind Commands.ClearSelection}"
ItemsSource="{x:Bind ViewModel.ShelfViewModel.Items}" />
From 2c7d8a8d299c588bf8250cb7609e4b57342906f0 Mon Sep 17 00:00:00 2001
From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com>
Date: Fri, 25 Jul 2025 01:28:13 +0200
Subject: [PATCH 3/6] Added "Open parent folder" menu item
---
src/Files.App/Data/Items/ShelfItem.cs | 15 +++++++++++++++
src/Files.App/UserControls/Pane/ShelfPane.xaml.cs | 10 ++++++++--
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/Files.App/Data/Items/ShelfItem.cs b/src/Files.App/Data/Items/ShelfItem.cs
index 3eb42efaa63f..0daaee3a833b 100644
--- a/src/Files.App/Data/Items/ShelfItem.cs
+++ b/src/Files.App/Data/Items/ShelfItem.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using Files.Shared.Utils;
+using System.Windows.Documents;
namespace Files.App.Data.Items
{
@@ -34,6 +35,20 @@ public async Task InitAsync(CancellationToken cancellationToken = default)
Icon = await _imageService.GetIconAsync(Inner, cancellationToken);
}
+ [RelayCommand]
+ public async Task ViewInFolderAsync(CancellationToken cancellationToken)
+ {
+ var context = Ioc.Default.GetRequiredService();
+ if (context.ShellPage is not { } shellPage)
+ return;
+
+ var parent = await Inner.GetParentAsync(cancellationToken);
+ if (parent is null)
+ return;
+
+ await NavigationHelpers.OpenPath(parent.Id, shellPage);
+ }
+
[RelayCommand]
public void Remove()
{
diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs
index 0f10ca60e8fc..41402ef106b9 100644
--- a/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs
+++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml.cs
@@ -95,11 +95,17 @@ widgetCardItem.DataContext is not ShelfItem item ||
var menuFlyout = new MenuFlyout();
- menuFlyout.Items.Add (new MenuFlyoutItem
+ menuFlyout.Items.Add(new MenuFlyoutItem
+ {
+ Text = Strings.BaseLayoutItemContextFlyoutOpenParentFolder_Text.GetLocalizedResource(),
+ Icon = new FontIcon() { Glyph = "\uE838" },
+ Command = item.ViewInFolderCommand
+ });
+ menuFlyout.Items.Add(new MenuFlyoutItem
{
Text = Strings.RemoveFromShelf.GetLocalizedResource(),
Icon = new FontIcon { Glyph = "\uE738" },
- Command = new RelayCommand(item.Remove)
+ Command = item.RemoveCommand
});
menuFlyout.ShowAt(widgetCardItem);
From dd156122e79053bf7d5696ae8fd0eea9b552848e Mon Sep 17 00:00:00 2001
From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com>
Date: Wed, 30 Jul 2025 02:03:15 +0200
Subject: [PATCH 4/6] Added bulk copy and cut actions
---
.../Transfer/BaseTransferItemAction.cs | 103 +----------
src/Files.App/Helpers/TransferHelpers.cs | 170 ++++++++++++++++++
.../UserControls/Pane/ShelfPane.xaml | 14 ++
.../UserControls/Pane/ShelfPane.xaml.cs | 16 ++
.../ViewModels/UserControls/ShelfViewModel.cs | 54 +++++-
src/Files.App/Views/MainPage.xaml | 2 +
6 files changed, 249 insertions(+), 110 deletions(-)
create mode 100644 src/Files.App/Helpers/TransferHelpers.cs
diff --git a/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs b/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs
index aa37f1fbd908..ebd1f4068c87 100644
--- a/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs
+++ b/src/Files.App/Actions/FileSystem/Transfer/BaseTransferItemAction.cs
@@ -1,12 +1,7 @@
// Copyright (c) Files Community
// Licensed under the MIT License.
-using Microsoft.Extensions.Logging;
-using System.Collections.Concurrent;
-using System.IO;
using Windows.ApplicationModel.DataTransfer;
-using Windows.Storage;
-using Windows.System;
namespace Files.App.Actions
{
@@ -25,103 +20,7 @@ public BaseTransferItemAction()
public async Task ExecuteTransferAsync(DataPackageOperation type = DataPackageOperation.Copy)
{
- if (ContentPageContext.ShellPage?.SlimContentPage is null ||
- ContentPageContext.ShellPage.SlimContentPage.IsItemSelected is false)
- return;
-
- // Reset cut mode
- ContentPageContext.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity();
-
- ConcurrentBag items = [];
- var itemsCount = ContentPageContext.SelectedItems.Count;
- var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null;
- var dataPackage = new DataPackage() { RequestedOperation = type };
-
- try
- {
- // Update the status to in-progress
- if (statusCenterItem is not null)
- {
- statusCenterItem.Progress.EnumerationCompleted = true;
- statusCenterItem.Progress.ItemsCount = items.Count;
- statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress);
- }
-
- await ContentPageContext.SelectedItems.ToList().ParallelForEachAsync(async listedItem =>
- {
- // Update the status to increase processed count by one
- if (statusCenterItem is not null)
- {
- statusCenterItem.Progress.AddProcessedItemsCount(1);
- statusCenterItem.Progress.Report();
- }
-
- if (listedItem is FtpItem ftpItem)
- {
- // Don't dim selected items here since FTP doesn't support cut
- if (ftpItem.PrimaryItemAttribute is StorageItemTypes.File or StorageItemTypes.Folder)
- items.Add(await ftpItem.ToStorageItem());
- }
- else
- {
- if (type is DataPackageOperation.Move)
- {
- // Dim opacities accordingly
- await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
- {
- listedItem.Opacity = Constants.UI.DimItemOpacity;
- });
- }
-
- FilesystemResult? result =
- listedItem.PrimaryItemAttribute == StorageItemTypes.File || listedItem is ZipItem
- ? await ContentPageContext.ShellPage.ShellViewModel.GetFileFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t))
- : await ContentPageContext.ShellPage.ShellViewModel.GetFolderFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t));
-
- if (!result)
- throw new IOException($"Failed to process {listedItem.ItemPath} in cutting/copying to the clipboard.", (int)result.ErrorCode);
- }
- },
- 10,
- statusCenterItem?.CancellationToken ?? default);
-
- var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder);
- if (standardObjectsOnly)
- items = new ConcurrentBag(await items.ToStandardStorageItemsAsync());
-
- if (items.IsEmpty)
- return;
-
- dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
- dataPackage.SetStorageItems(items, false);
-
- Clipboard.SetContent(dataPackage);
- }
- catch (Exception ex)
- {
- dataPackage = default;
-
- if (ex is not IOException)
- App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error.");
-
- if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized)
- {
- string[] filePaths = ContentPageContext.SelectedItems.Select(x => x.ItemPath).ToArray();
- await FileOperationsHelpers.SetClipboard(filePaths, type);
-
- return;
- }
-
- // Reset cut mode
- ContentPageContext.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity();
-
- return;
- }
- finally
- {
- if (statusCenterItem is not null)
- StatusCenterViewModel.RemoveItem(statusCenterItem);
- }
+ await TransferHelpers.ExecuteTransferAsync(ContentPageContext, StatusCenterViewModel, type);
}
private void ContentPageContext_PropertyChanged(object? sender, PropertyChangedEventArgs e)
diff --git a/src/Files.App/Helpers/TransferHelpers.cs b/src/Files.App/Helpers/TransferHelpers.cs
new file mode 100644
index 000000000000..7f60fad9945b
--- /dev/null
+++ b/src/Files.App/Helpers/TransferHelpers.cs
@@ -0,0 +1,170 @@
+using Microsoft.Extensions.Logging;
+using System.Collections.Concurrent;
+using Windows.ApplicationModel.DataTransfer;
+using Windows.Storage;
+
+namespace Files.App.Helpers
+{
+ public static class TransferHelpers
+ {
+ public static async Task ExecuteTransferAsync(IReadOnlyList itemsToTransfer, ShellViewModel shellViewModel, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy)
+ {
+ ConcurrentBag items = [];
+ var itemsCount = itemsToTransfer.Count;
+ var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null;
+ var dataPackage = new DataPackage() { RequestedOperation = type };
+
+ try
+ {
+ // Update the status to in-progress
+ if (statusCenterItem is not null)
+ {
+ statusCenterItem.Progress.EnumerationCompleted = true;
+ statusCenterItem.Progress.ItemsCount = items.Count;
+ statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress);
+ }
+
+ await itemsToTransfer.ParallelForEachAsync(async storable =>
+ {
+ // Update the status to increase processed count by one
+ if (statusCenterItem is not null)
+ {
+ statusCenterItem.Progress.AddProcessedItemsCount(1);
+ statusCenterItem.Progress.Report();
+ }
+
+ var result = storable switch
+ {
+ IFile => await shellViewModel.GetFileFromPathAsync(storable.Id).OnSuccess(x => items.Add(x)),
+ IFolder => await shellViewModel.GetFolderFromPathAsync(storable.Id).OnSuccess(x => items.Add(x)),
+ };
+
+ if (!result)
+ throw new SystemIO.IOException($"Failed to process {storable.Id} in cutting/copying to the clipboard.", (int)result.ErrorCode);
+ }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None);
+
+ var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder);
+ if (standardObjectsOnly)
+ items = new(await items.ToStandardStorageItemsAsync());
+
+ if (items.IsEmpty)
+ return;
+
+ dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
+ dataPackage.SetStorageItems(items, false);
+
+ Clipboard.SetContent(dataPackage);
+ }
+ catch (Exception ex)
+ {
+ if (ex is not SystemIO.IOException)
+ App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error.");
+
+ if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized)
+ {
+ var filePaths = itemsToTransfer.Select(x => x.Id).ToArray();
+ await FileOperationsHelpers.SetClipboard(filePaths, type);
+ }
+ }
+ finally
+ {
+ if (statusCenterItem is not null)
+ statusViewModel.RemoveItem(statusCenterItem);
+ }
+ }
+
+ public static async Task ExecuteTransferAsync(IContentPageContext context, StatusCenterViewModel statusViewModel, DataPackageOperation type = DataPackageOperation.Copy)
+ {
+ if (context.ShellPage?.SlimContentPage is null ||
+ context.ShellPage.SlimContentPage.IsItemSelected is false)
+ return;
+
+ // Reset cut mode
+ context.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity();
+
+ ConcurrentBag items = [];
+ var itemsCount = context.SelectedItems.Count;
+ var statusCenterItem = itemsCount > 50 ? StatusCenterHelper.AddCard_Prepare() : null;
+ var dataPackage = new DataPackage() { RequestedOperation = type };
+
+ try
+ {
+ // Update the status to in-progress
+ if (statusCenterItem is not null)
+ {
+ statusCenterItem.Progress.EnumerationCompleted = true;
+ statusCenterItem.Progress.ItemsCount = items.Count;
+ statusCenterItem.Progress.ReportStatus(FileSystemStatusCode.InProgress);
+ }
+
+ await context.SelectedItems.ToList().ParallelForEachAsync(async listedItem =>
+ {
+ // Update the status to increase processed count by one
+ if (statusCenterItem is not null)
+ {
+ statusCenterItem.Progress.AddProcessedItemsCount(1);
+ statusCenterItem.Progress.Report();
+ }
+
+ if (listedItem is FtpItem ftpItem)
+ {
+ // Don't dim selected items here since FTP doesn't support cut
+ if (ftpItem.PrimaryItemAttribute is StorageItemTypes.File or StorageItemTypes.Folder)
+ items.Add(await ftpItem.ToStorageItem());
+ }
+ else
+ {
+ if (type is DataPackageOperation.Move)
+ {
+ // Dim opacities accordingly
+ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() =>
+ {
+ listedItem.Opacity = Constants.UI.DimItemOpacity;
+ });
+ }
+
+ var result = listedItem.PrimaryItemAttribute == StorageItemTypes.File || listedItem is ZipItem
+ ? await context.ShellPage.ShellViewModel.GetFileFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t))
+ : await context.ShellPage.ShellViewModel.GetFolderFromPathAsync(listedItem.ItemPath).OnSuccess(t => items.Add(t));
+
+ if (!result)
+ throw new SystemIO.IOException($"Failed to process {listedItem.ItemPath} in cutting/copying to the clipboard.", (int)result.ErrorCode);
+ }
+ }, 10, statusCenterItem?.CancellationToken ?? CancellationToken.None);
+
+ var standardObjectsOnly = items.All(x => x is StorageFile or StorageFolder or SystemStorageFile or SystemStorageFolder);
+ if (standardObjectsOnly)
+ items = new(await items.ToStandardStorageItemsAsync());
+
+ if (items.IsEmpty)
+ return;
+
+ dataPackage.Properties.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
+ dataPackage.SetStorageItems(items, false);
+
+ Clipboard.SetContent(dataPackage);
+ }
+ catch (Exception ex)
+ {
+ if (ex is not SystemIO.IOException)
+ App.Logger.LogWarning(ex, "Failed to process cutting/copying due to an unknown error.");
+
+ if ((FileSystemStatusCode)ex.HResult is FileSystemStatusCode.Unauthorized)
+ {
+ var filePaths = context.SelectedItems.Select(x => x.ItemPath).ToArray();
+ await FileOperationsHelpers.SetClipboard(filePaths, type);
+
+ return;
+ }
+
+ // Reset cut mode
+ context.ShellPage.SlimContentPage.ItemManipulationModel.RefreshItemsOpacity();
+ }
+ finally
+ {
+ if (statusCenterItem is not null)
+ statusViewModel.RemoveItem(statusCenterItem);
+ }
+ }
+ }
+}
diff --git a/src/Files.App/UserControls/Pane/ShelfPane.xaml b/src/Files.App/UserControls/Pane/ShelfPane.xaml
index bd55bb5393bc..fe4935830e57 100644
--- a/src/Files.App/UserControls/Pane/ShelfPane.xaml
+++ b/src/Files.App/UserControls/Pane/ShelfPane.xaml
@@ -164,6 +164,20 @@
Command="{x:Bind BulkDeleteCommand, Mode=OneWay}">
+
+
(ICommand?)GetValue(BulkCopyCommandProperty);
+ set => SetValue(BulkCopyCommandProperty, value);
+ }
+ public static readonly DependencyProperty BulkCopyCommandProperty =
+ DependencyProperty.Register(nameof(BulkCopyCommand), typeof(ICommand), typeof(ShelfPane), new PropertyMetadata(null));
+
+ public ICommand? BulkCutCommand
+ {
+ get => (ICommand?)GetValue(BulkCutCommandProperty);
+ set => SetValue(BulkCutCommandProperty, value);
+ }
+ public static readonly DependencyProperty BulkCutCommandProperty =
+ DependencyProperty.Register(nameof(BulkCutCommand), typeof(ICommand), typeof(ShelfPane), new PropertyMetadata(null));
+
public ICommand? ItemFocusedCommand
{
get => (ICommand?)GetValue(ItemFocusedCommandProperty);
diff --git a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
index ddb778bdd047..832b9c5b0e07 100644
--- a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
@@ -3,13 +3,19 @@
using Files.Shared.Utils;
using System.Collections.Specialized;
+using Windows.ApplicationModel.DataTransfer;
namespace Files.App.ViewModels.UserControls
{
+ public sealed record class WatcherReference(IFolderWatcher FolderWatcher, int ReferenceCount)
+ {
+ public int ReferenceCount { get; set; } = ReferenceCount;
+ }
+
[Bindable(true)]
public sealed partial class ShelfViewModel : ObservableObject, IAsyncInitialize
{
- private readonly Dictionary _watchers;
+ private readonly Dictionary _watchers;
public ObservableCollection Items { get; }
@@ -55,6 +61,38 @@ private async Task BulkDeleteAsync()
await shellPage.ShellViewModel.ApplyFilesAndFoldersChangesAsync();
}
+ [RelayCommand]
+ private async Task BulkCopyAsync()
+ {
+ if (Items.IsEmpty())
+ return;
+
+ var context = Ioc.Default.GetRequiredService();
+ var statusViewModel = Ioc.Default.GetRequiredService();
+
+ if (context.ShellPage?.ShellViewModel is not { } shellViewModel)
+ return;
+
+ var itemsToCopy = Items.Select(x => x.Inner).ToArray();
+ await TransferHelpers.ExecuteTransferAsync(itemsToCopy, shellViewModel, statusViewModel, DataPackageOperation.Copy);
+ }
+
+ [RelayCommand]
+ private async Task BulkCutAsync()
+ {
+ if (Items.IsEmpty())
+ return;
+
+ var context = Ioc.Default.GetRequiredService();
+ var statusViewModel = Ioc.Default.GetRequiredService();
+
+ if (context.ShellPage?.ShellViewModel is not { } shellViewModel)
+ return;
+
+ var itemsToCut = Items.Select(x => x.Inner).ToArray();
+ await TransferHelpers.ExecuteTransferAsync(itemsToCut, shellViewModel, statusViewModel, DataPackageOperation.Move);
+ }
+
private async void Items_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
@@ -68,7 +106,7 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
if (_watchers.TryGetValue(parentPath, out var reference))
{
// Only increase the reference count if the watcher already exists
- reference.Item2 += 1;
+ reference.ReferenceCount += 1;
return;
}
@@ -79,7 +117,7 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
var watcher = await mutableFolder.GetFolderWatcherAsync();
watcher.CollectionChanged += Watcher_CollectionChanged;
- _watchers.Add(parentPath, (watcher, 1));
+ _watchers.Add(parentPath, new(watcher, 1));
break;
}
@@ -93,11 +131,11 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
return;
// Decrease the reference count and remove the watcher if no references are present
- reference.Item2 -= 1;
- if (reference.Item2 < 1)
+ reference.ReferenceCount -= 1;
+ if (reference.ReferenceCount < 1)
{
- reference.Item1.CollectionChanged -= Watcher_CollectionChanged;
- reference.Item1.Dispose();
+ reference.FolderWatcher.CollectionChanged -= Watcher_CollectionChanged;
+ reference.FolderWatcher.Dispose();
_watchers.Remove(parentPath);
}
@@ -108,7 +146,7 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
private async void Watcher_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
- if (sender is not IFolderWatcher watcher)
+ if (sender is not IFolderWatcher)
return;
switch (e.Action)
diff --git a/src/Files.App/Views/MainPage.xaml b/src/Files.App/Views/MainPage.xaml
index 7e1e86801cbd..141738bf980e 100644
--- a/src/Files.App/Views/MainPage.xaml
+++ b/src/Files.App/Views/MainPage.xaml
@@ -271,6 +271,8 @@
Grid.Column="3"
Margin="4,0,0,8"
x:Load="{x:Bind ViewModel.ShowShelfPane, Mode=OneWay}"
+ BulkCopyCommand="{x:Bind ViewModel.ShelfViewModel.BulkCopyCommand}"
+ BulkCutCommand="{x:Bind ViewModel.ShelfViewModel.BulkCutCommand}"
BulkDeleteCommand="{x:Bind ViewModel.ShelfViewModel.BulkDeleteCommand}"
ClearCommand="{x:Bind ViewModel.ShelfViewModel.ClearItemsCommand}"
ItemFocusedCommand="{x:Bind Commands.ClearSelection}"
From cde91a5d7500d7eb0594909e4c49278a16482479 Mon Sep 17 00:00:00 2001
From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com>
Date: Wed, 30 Jul 2025 02:10:13 +0200
Subject: [PATCH 5/6] Update ShelfViewModel.cs
---
.../ViewModels/UserControls/ShelfViewModel.cs | 48 +++++++++----------
1 file changed, 24 insertions(+), 24 deletions(-)
diff --git a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
index beaad87dc3b7..832b9c5b0e07 100644
--- a/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
+++ b/src/Files.App/ViewModels/UserControls/ShelfViewModel.cs
@@ -98,9 +98,9 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
switch (e.Action)
{
case NotifyCollectionChangedAction.Add when e.NewItems is not null:
- {
- if (e.NewItems[0] is not ShelfItem shelfItem)
- return;
+ {
+ if (e.NewItems[0] is not ShelfItem shelfItem)
+ return;
var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty;
if (_watchers.TryGetValue(parentPath, out var reference))
@@ -113,22 +113,22 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
if (await shelfItem.Inner.GetParentAsync() is not IMutableFolder mutableFolder)
return;
- // Register new watcher
- var watcher = await mutableFolder.GetFolderWatcherAsync();
- watcher.CollectionChanged += Watcher_CollectionChanged;
+ // Register new watcher
+ var watcher = await mutableFolder.GetFolderWatcherAsync();
+ watcher.CollectionChanged += Watcher_CollectionChanged;
_watchers.Add(parentPath, new(watcher, 1));
break;
}
case NotifyCollectionChangedAction.Remove when e.OldItems is not null:
- {
- if (e.OldItems[0] is not ShelfItem shelfItem)
- return;
+ {
+ if (e.OldItems[0] is not ShelfItem shelfItem)
+ return;
- var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty;
- if (!_watchers.TryGetValue(parentPath, out var reference))
- return;
+ var parentPath = SystemIO.Path.GetDirectoryName(shelfItem.Inner.Id) ?? string.Empty;
+ if (!_watchers.TryGetValue(parentPath, out var reference))
+ return;
// Decrease the reference count and remove the watcher if no references are present
reference.ReferenceCount -= 1;
@@ -139,8 +139,8 @@ private async void Items_CollectionChanged(object? sender, NotifyCollectionChang
_watchers.Remove(parentPath);
}
- break;
- }
+ break;
+ }
}
}
@@ -152,16 +152,16 @@ private async void Watcher_CollectionChanged(object? sender, NotifyCollectionCha
switch (e.Action)
{
case NotifyCollectionChangedAction.Remove when e.OldItems is not null:
- {
- // Remove the matching item notified from the watcher
- var item = e.OldItems.Cast().ElementAt(0);
- var itemToRemove = Items.FirstOrDefault(x => x.Inner.Id == item.Id);
- if (itemToRemove is null)
- return;
-
- await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => Items.Remove(itemToRemove));
- break;
- }
+ {
+ // Remove the matching item notified from the watcher
+ var item = e.OldItems.Cast().ElementAt(0);
+ var itemToRemove = Items.FirstOrDefault(x => x.Inner.Id == item.Id);
+ if (itemToRemove is null)
+ return;
+
+ await MainWindow.Instance.DispatcherQueue.EnqueueOrInvokeAsync(() => Items.Remove(itemToRemove));
+ break;
+ }
}
}
}
From 51a88fdb9ccb72722ab289181a726bf03d1f36aa Mon Sep 17 00:00:00 2001
From: d2dyno006 <53011783+d2dyno006@users.noreply.github.com>
Date: Mon, 4 Aug 2025 01:50:10 +0200
Subject: [PATCH 6/6] Update ToggleShelfPaneAction.cs
---
src/Files.App/Actions/Show/ToggleShelfPaneAction.cs | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs b/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs
index 0a4a1e6a5706..93242351c900 100644
--- a/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs
+++ b/src/Files.App/Actions/Show/ToggleShelfPaneAction.cs
@@ -15,7 +15,15 @@ public string Description
public RichGlyph Glyph
=> new(themedIconStyle: "App.ThemedIcons.Shelf");
-
+
+ // TODO Remove IsAccessibleGlobally when shelf feature is ready
+ public bool IsAccessibleGlobally
+ => AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev;
+
+ // TODO Remove IsExecutable when shelf feature is ready
+ public bool IsExecutable
+ => AppLifecycleHelper.AppEnvironment is AppEnvironment.Dev;
+
public bool IsOn
=> generalSettingsService.ShowShelfPane;