From a0c71166a83625a1a8cd21f94a1fa9adc62435ad Mon Sep 17 00:00:00 2001 From: Devin <93222657+devin-slothower@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:39:18 -0400 Subject: [PATCH] Feature: Added experimental support for flattening folders (#15992) --- .../Actions/FileSystem/FlattenFolderAction.cs | 139 ++++++++++++++++++ .../Data/Commands/Manager/CommandCodes.cs | 3 + .../Data/Commands/Manager/CommandManager.cs | 6 +- .../Data/Commands/Manager/ICommandManager.cs | 2 + .../Data/Contracts/IGeneralSettingsService.cs | 5 + .../ContentPageContextFlyoutFactory.cs | 3 +- .../Settings/GeneralSettingsService.cs | 6 + src/Files.App/Strings/en-US/Resources.resw | 18 +++ .../ViewModels/Settings/AdvancedViewModel.cs | 14 ++ .../ViewModels/Settings/GeneralViewModel.cs | 14 ++ .../Views/Settings/AdvancedPage.xaml | 14 ++ src/Files.App/Views/Settings/GeneralPage.xaml | 9 ++ 12 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 src/Files.App/Actions/FileSystem/FlattenFolderAction.cs diff --git a/src/Files.App/Actions/FileSystem/FlattenFolderAction.cs b/src/Files.App/Actions/FileSystem/FlattenFolderAction.cs new file mode 100644 index 0000000000000..b670ed7a2ffb3 --- /dev/null +++ b/src/Files.App/Actions/FileSystem/FlattenFolderAction.cs @@ -0,0 +1,139 @@ +// Copyright (c) 2024 Files Community +// Licensed under the MIT License. See the LICENSE. + +using Microsoft.Extensions.Logging; +using Microsoft.UI.Xaml.Controls; +using System.IO; +using Windows.Foundation.Metadata; +using Windows.Storage; + +namespace Files.App.Actions +{ + internal sealed class FlattenFolderAction : ObservableObject, IAction + { + private readonly IContentPageContext context; + private readonly IGeneralSettingsService GeneralSettingsService = Ioc.Default.GetRequiredService(); + + public string Label + => "FlattenFolder".GetLocalizedResource(); + + public string Description + => "FlattenFolderDescription".GetLocalizedResource(); + + public RichGlyph Glyph + => new(themedIconStyle: "App.ThemedIcons.Folder"); + + public bool IsExecutable => + GeneralSettingsService.ShowFlattenOptions && + context.ShellPage is not null && + context.HasSelection && + context.SelectedItems.Count is 1 && + context.SelectedItem is not null && + context.SelectedItem.PrimaryItemAttribute is StorageItemTypes.Folder; + + public FlattenFolderAction() + { + context = Ioc.Default.GetRequiredService(); + + context.PropertyChanged += Context_PropertyChanged; + GeneralSettingsService.PropertyChanged += GeneralSettingsService_PropertyChanged; + } + + public async Task ExecuteAsync(object? parameter = null) + { + if (context.SelectedItem is null) + return; + + var optionsDialog = new ContentDialog() + { + Title = "FlattenFolder".GetLocalizedResource(), + Content = "FlattenFolderDialogContent".GetLocalizedResource(), + PrimaryButtonText = "Flatten".GetLocalizedResource(), + SecondaryButtonText = "Cancel".GetLocalizedResource(), + DefaultButton = ContentDialogButton.Primary + }; + + if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8)) + optionsDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot; + + var result = await optionsDialog.TryShowAsync(); + if (result != ContentDialogResult.Primary) + return; + + FlattenFolder(context.SelectedItem.ItemPath); + } + + private void FlattenFolder(string path) + { + var containedFolders = Directory.GetDirectories(path); + var containedFiles = Directory.GetFiles(path); + + foreach (var containedFolder in containedFolders) + { + FlattenFolder(containedFolder); + + var folderName = Path.GetFileName(containedFolder); + var destinationPath = Path.Combine(context?.SelectedItem?.ItemPath ?? string.Empty, folderName); + + if (Directory.Exists(destinationPath)) + continue; + + try + { + Directory.Move(containedFolder, destinationPath); + } + catch (Exception ex) + { + App.Logger.LogWarning(ex.Message, $"Folder '{folderName}' already exists in the destination folder."); + } + } + + foreach (var containedFile in containedFiles) + { + var fileName = Path.GetFileName(containedFile); + var destinationPath = Path.Combine(context?.SelectedItem?.ItemPath ?? string.Empty, fileName); + + if (File.Exists(destinationPath)) + continue; + + try + { + File.Move(containedFile, destinationPath); + } + catch (Exception ex) + { + App.Logger.LogWarning(ex.Message, $"Failed to move file '{fileName}'."); + } + } + + if (Directory.GetFiles(path).Length == 0 && Directory.GetDirectories(path).Length == 0) + { + try + { + Directory.Delete(path); + } + catch (Exception ex) + { + App.Logger.LogWarning(ex.Message, $"Failed to delete folder '{path}'."); + } + } + } + + private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + switch (e.PropertyName) + { + case nameof(IContentPageContext.HasSelection): + case nameof(IContentPageContext.SelectedItem): + OnPropertyChanged(nameof(IsExecutable)); + break; + } + } + + private void GeneralSettingsService_PropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(IGeneralSettingsService.ShowFlattenOptions)) + OnPropertyChanged(nameof(IsExecutable)); + } + } +} diff --git a/src/Files.App/Data/Commands/Manager/CommandCodes.cs b/src/Files.App/Data/Commands/Manager/CommandCodes.cs index 18a530ea2d025..60310291a9901 100644 --- a/src/Files.App/Data/Commands/Manager/CommandCodes.cs +++ b/src/Files.App/Data/Commands/Manager/CommandCodes.cs @@ -97,6 +97,9 @@ public enum CommandCodes DecompressArchiveHereSmart, DecompressArchiveToChildFolder, + // Folders + FlattenFolder, + // Image Manipulation RotateLeft, RotateRight, diff --git a/src/Files.App/Data/Commands/Manager/CommandManager.cs b/src/Files.App/Data/Commands/Manager/CommandManager.cs index e319ba669aa64..c22dfeffd0e64 100644 --- a/src/Files.App/Data/Commands/Manager/CommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/CommandManager.cs @@ -1,10 +1,10 @@ // Copyright (c) 2024 Files Community // Licensed under the MIT License. See the LICENSE. -using System.Collections.Frozen; -using System.Collections.Immutable; using Files.App.Actions; using Microsoft.Extensions.Logging; +using System.Collections.Frozen; +using System.Collections.Immutable; namespace Files.App.Data.Commands { @@ -102,6 +102,7 @@ public IRichCommand this[HotKey hotKey] public IRichCommand DecompressArchiveHere => commands[CommandCodes.DecompressArchiveHere]; public IRichCommand DecompressArchiveHereSmart => commands[CommandCodes.DecompressArchiveHereSmart]; public IRichCommand DecompressArchiveToChildFolder => commands[CommandCodes.DecompressArchiveToChildFolder]; + public IRichCommand FlattenFolder => commands[CommandCodes.FlattenFolder]; public IRichCommand RotateLeft => commands[CommandCodes.RotateLeft]; public IRichCommand RotateRight => commands[CommandCodes.RotateRight]; public IRichCommand OpenItem => commands[CommandCodes.OpenItem]; @@ -292,6 +293,7 @@ public IEnumerator GetEnumerator() => [CommandCodes.DecompressArchiveHere] = new DecompressArchiveHere(), [CommandCodes.DecompressArchiveHereSmart] = new DecompressArchiveHereSmart(), [CommandCodes.DecompressArchiveToChildFolder] = new DecompressArchiveToChildFolderAction(), + [CommandCodes.FlattenFolder] = new FlattenFolderAction(), [CommandCodes.RotateLeft] = new RotateLeftAction(), [CommandCodes.RotateRight] = new RotateRightAction(), [CommandCodes.OpenItem] = new OpenItemAction(), diff --git a/src/Files.App/Data/Commands/Manager/ICommandManager.cs b/src/Files.App/Data/Commands/Manager/ICommandManager.cs index d572e35b8279a..971787c04e19d 100644 --- a/src/Files.App/Data/Commands/Manager/ICommandManager.cs +++ b/src/Files.App/Data/Commands/Manager/ICommandManager.cs @@ -89,6 +89,8 @@ public interface ICommandManager : IEnumerable IRichCommand DecompressArchiveHereSmart { get; } IRichCommand DecompressArchiveToChildFolder { get; } + IRichCommand FlattenFolder { get; } + IRichCommand RotateLeft { get; } IRichCommand RotateRight { get; } diff --git a/src/Files.App/Data/Contracts/IGeneralSettingsService.cs b/src/Files.App/Data/Contracts/IGeneralSettingsService.cs index 7235767ac91e2..ca8e6ebdf3e31 100644 --- a/src/Files.App/Data/Contracts/IGeneralSettingsService.cs +++ b/src/Files.App/Data/Contracts/IGeneralSettingsService.cs @@ -225,6 +225,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty /// bool ShowCompressionOptions { get; set; } + /// + /// Gets or sets a value indicating whether or not to show the flatten options e.g. single, recursive. + /// + bool ShowFlattenOptions { get; set; } + /// /// Gets or sets a value indicating whether or not to show the Send To menu. /// diff --git a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs index b9e435b2bb9bf..ae5ccdc87359c 100644 --- a/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs +++ b/src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs @@ -547,6 +547,7 @@ public static List GetBaseItemMenuItems( ], ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && StorageArchiveService.CanDecompress(selectedItems) }, + new ContextMenuFlyoutItemViewModelBuilder(Commands.FlattenFolder).Build(), new ContextMenuFlyoutItemViewModel() { Text = "SendTo".GetLocalizedResource(), @@ -587,7 +588,7 @@ public static List GetBaseItemMenuItems( ShowItem = isDriveRoot, IsEnabled = false }, - new ContextMenuFlyoutItemViewModelBuilder(Commands.EditInNotepad).Build(), + new ContextMenuFlyoutItemViewModelBuilder(Commands.EditInNotepad).Build(), new ContextMenuFlyoutItemViewModel() { ItemType = ContextMenuFlyoutItemType.Separator, diff --git a/src/Files.App/Services/Settings/GeneralSettingsService.cs b/src/Files.App/Services/Settings/GeneralSettingsService.cs index c8b4ee4395619..dfe850dc337f4 100644 --- a/src/Files.App/Services/Settings/GeneralSettingsService.cs +++ b/src/Files.App/Services/Settings/GeneralSettingsService.cs @@ -239,6 +239,12 @@ public bool ShowCompressionOptions set => Set(value); } + public bool ShowFlattenOptions + { + get => Get(false); + set => Set(value); + } + public bool ShowSendToMenu { get => Get(true); diff --git a/src/Files.App/Strings/en-US/Resources.resw b/src/Files.App/Strings/en-US/Resources.resw index 61ba1bc9322d0..ce3328fb242af 100644 --- a/src/Files.App/Strings/en-US/Resources.resw +++ b/src/Files.App/Strings/en-US/Resources.resw @@ -1853,6 +1853,9 @@ This option modifies the system registry and can have unexpected side effects on your device. Continue at your own risk. + + The flatten operations are permanent and not recommended. Continue at your own risk. + Create Library @@ -2027,6 +2030,21 @@ Compress + + Move all contents from subfolders into the selected location + + + Flatten folder + + + Flatten + + + Flattening a folder will move all contents from its subfolders to the selected location. This operation is permanent and cannot be undone. By using this experimental feature, you acknowledge the risk and agree not to hold the Files team responsible for any data loss. + + + Show flatten options + Select files and folders when hovering over them diff --git a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs index 992f7922a7329..2de96f76ca49d 100644 --- a/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs +++ b/src/Files.App/ViewModels/Settings/AdvancedViewModel.cs @@ -348,6 +348,20 @@ public bool ShowSystemTrayIcon } } + // TODO remove when feature is marked as stable + public bool ShowFlattenOptions + { + get => UserSettingsService.GeneralSettingsService.ShowFlattenOptions; + set + { + if (value == UserSettingsService.GeneralSettingsService.ShowFlattenOptions) + return; + + UserSettingsService.GeneralSettingsService.ShowFlattenOptions = value; + OnPropertyChanged(); + } + } + public async Task OpenFilesOnWindowsStartupAsync() { var stateMode = await ReadState(); diff --git a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs index f82c9308d9efa..6713d39dd31e5 100644 --- a/src/Files.App/ViewModels/Settings/GeneralViewModel.cs +++ b/src/Files.App/ViewModels/Settings/GeneralViewModel.cs @@ -454,6 +454,20 @@ public bool ShowCompressionOptions } } + // TODO uncomment code when feature is marked as stable + //public bool ShowFlattenOptions + //{ + // get => UserSettingsService.GeneralSettingsService.ShowFlattenOptions; + // set + // { + // if (value == UserSettingsService.GeneralSettingsService.ShowFlattenOptions) + // return; + + // UserSettingsService.GeneralSettingsService.ShowFlattenOptions = value; + // OnPropertyChanged(); + // } + //} + public bool ShowSendToMenu { get => UserSettingsService.GeneralSettingsService.ShowSendToMenu; diff --git a/src/Files.App/Views/Settings/AdvancedPage.xaml b/src/Files.App/Views/Settings/AdvancedPage.xaml index 93cb118541610..eebca148bf40d 100644 --- a/src/Files.App/Views/Settings/AdvancedPage.xaml +++ b/src/Files.App/Views/Settings/AdvancedPage.xaml @@ -189,6 +189,20 @@ + + + + + + + + diff --git a/src/Files.App/Views/Settings/GeneralPage.xaml b/src/Files.App/Views/Settings/GeneralPage.xaml index 5429ca806f76d..466711cf696dc 100644 --- a/src/Files.App/Views/Settings/GeneralPage.xaml +++ b/src/Files.App/Views/Settings/GeneralPage.xaml @@ -325,6 +325,15 @@ Style="{StaticResource RightAlignedToggleSwitchStyle}" /> + + + +