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;