Skip to content

Commit 8f792f4

Browse files
Feature: Added experimental support for flattening folders (#15992)
1 parent 7560e51 commit 8f792f4

File tree

12 files changed

+230
-3
lines changed

12 files changed

+230
-3
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Microsoft.Extensions.Logging;
5+
using Microsoft.UI.Xaml.Controls;
6+
using System.IO;
7+
using Windows.Foundation.Metadata;
8+
using Windows.Storage;
9+
10+
namespace Files.App.Actions
11+
{
12+
internal sealed class FlattenFolderAction : ObservableObject, IAction
13+
{
14+
private readonly IContentPageContext context;
15+
private readonly IGeneralSettingsService GeneralSettingsService = Ioc.Default.GetRequiredService<IGeneralSettingsService>();
16+
17+
public string Label
18+
=> "FlattenFolder".GetLocalizedResource();
19+
20+
public string Description
21+
=> "FlattenFolderDescription".GetLocalizedResource();
22+
23+
public RichGlyph Glyph
24+
=> new(themedIconStyle: "App.ThemedIcons.Folder");
25+
26+
public bool IsExecutable =>
27+
GeneralSettingsService.ShowFlattenOptions &&
28+
context.ShellPage is not null &&
29+
context.HasSelection &&
30+
context.SelectedItems.Count is 1 &&
31+
context.SelectedItem is not null &&
32+
context.SelectedItem.PrimaryItemAttribute is StorageItemTypes.Folder;
33+
34+
public FlattenFolderAction()
35+
{
36+
context = Ioc.Default.GetRequiredService<IContentPageContext>();
37+
38+
context.PropertyChanged += Context_PropertyChanged;
39+
GeneralSettingsService.PropertyChanged += GeneralSettingsService_PropertyChanged;
40+
}
41+
42+
public async Task ExecuteAsync(object? parameter = null)
43+
{
44+
if (context.SelectedItem is null)
45+
return;
46+
47+
var optionsDialog = new ContentDialog()
48+
{
49+
Title = "FlattenFolder".GetLocalizedResource(),
50+
Content = "FlattenFolderDialogContent".GetLocalizedResource(),
51+
PrimaryButtonText = "Flatten".GetLocalizedResource(),
52+
SecondaryButtonText = "Cancel".GetLocalizedResource(),
53+
DefaultButton = ContentDialogButton.Primary
54+
};
55+
56+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
57+
optionsDialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
58+
59+
var result = await optionsDialog.TryShowAsync();
60+
if (result != ContentDialogResult.Primary)
61+
return;
62+
63+
FlattenFolder(context.SelectedItem.ItemPath);
64+
}
65+
66+
private void FlattenFolder(string path)
67+
{
68+
var containedFolders = Directory.GetDirectories(path);
69+
var containedFiles = Directory.GetFiles(path);
70+
71+
foreach (var containedFolder in containedFolders)
72+
{
73+
FlattenFolder(containedFolder);
74+
75+
var folderName = Path.GetFileName(containedFolder);
76+
var destinationPath = Path.Combine(context?.SelectedItem?.ItemPath ?? string.Empty, folderName);
77+
78+
if (Directory.Exists(destinationPath))
79+
continue;
80+
81+
try
82+
{
83+
Directory.Move(containedFolder, destinationPath);
84+
}
85+
catch (Exception ex)
86+
{
87+
App.Logger.LogWarning(ex.Message, $"Folder '{folderName}' already exists in the destination folder.");
88+
}
89+
}
90+
91+
foreach (var containedFile in containedFiles)
92+
{
93+
var fileName = Path.GetFileName(containedFile);
94+
var destinationPath = Path.Combine(context?.SelectedItem?.ItemPath ?? string.Empty, fileName);
95+
96+
if (File.Exists(destinationPath))
97+
continue;
98+
99+
try
100+
{
101+
File.Move(containedFile, destinationPath);
102+
}
103+
catch (Exception ex)
104+
{
105+
App.Logger.LogWarning(ex.Message, $"Failed to move file '{fileName}'.");
106+
}
107+
}
108+
109+
if (Directory.GetFiles(path).Length == 0 && Directory.GetDirectories(path).Length == 0)
110+
{
111+
try
112+
{
113+
Directory.Delete(path);
114+
}
115+
catch (Exception ex)
116+
{
117+
App.Logger.LogWarning(ex.Message, $"Failed to delete folder '{path}'.");
118+
}
119+
}
120+
}
121+
122+
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
123+
{
124+
switch (e.PropertyName)
125+
{
126+
case nameof(IContentPageContext.HasSelection):
127+
case nameof(IContentPageContext.SelectedItem):
128+
OnPropertyChanged(nameof(IsExecutable));
129+
break;
130+
}
131+
}
132+
133+
private void GeneralSettingsService_PropertyChanged(object? sender, PropertyChangedEventArgs e)
134+
{
135+
if (e.PropertyName is nameof(IGeneralSettingsService.ShowFlattenOptions))
136+
OnPropertyChanged(nameof(IsExecutable));
137+
}
138+
}
139+
}

src/Files.App/Data/Commands/Manager/CommandCodes.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ public enum CommandCodes
9797
DecompressArchiveHereSmart,
9898
DecompressArchiveToChildFolder,
9999

100+
// Folders
101+
FlattenFolder,
102+
100103
// Image Manipulation
101104
RotateLeft,
102105
RotateRight,

src/Files.App/Data/Commands/Manager/CommandManager.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// Copyright (c) 2024 Files Community
22
// Licensed under the MIT License. See the LICENSE.
33

4-
using System.Collections.Frozen;
5-
using System.Collections.Immutable;
64
using Files.App.Actions;
75
using Microsoft.Extensions.Logging;
6+
using System.Collections.Frozen;
7+
using System.Collections.Immutable;
88

99
namespace Files.App.Data.Commands
1010
{
@@ -102,6 +102,7 @@ public IRichCommand this[HotKey hotKey]
102102
public IRichCommand DecompressArchiveHere => commands[CommandCodes.DecompressArchiveHere];
103103
public IRichCommand DecompressArchiveHereSmart => commands[CommandCodes.DecompressArchiveHereSmart];
104104
public IRichCommand DecompressArchiveToChildFolder => commands[CommandCodes.DecompressArchiveToChildFolder];
105+
public IRichCommand FlattenFolder => commands[CommandCodes.FlattenFolder];
105106
public IRichCommand RotateLeft => commands[CommandCodes.RotateLeft];
106107
public IRichCommand RotateRight => commands[CommandCodes.RotateRight];
107108
public IRichCommand OpenItem => commands[CommandCodes.OpenItem];
@@ -292,6 +293,7 @@ public IEnumerator<IRichCommand> GetEnumerator() =>
292293
[CommandCodes.DecompressArchiveHere] = new DecompressArchiveHere(),
293294
[CommandCodes.DecompressArchiveHereSmart] = new DecompressArchiveHereSmart(),
294295
[CommandCodes.DecompressArchiveToChildFolder] = new DecompressArchiveToChildFolderAction(),
296+
[CommandCodes.FlattenFolder] = new FlattenFolderAction(),
295297
[CommandCodes.RotateLeft] = new RotateLeftAction(),
296298
[CommandCodes.RotateRight] = new RotateRightAction(),
297299
[CommandCodes.OpenItem] = new OpenItemAction(),

src/Files.App/Data/Commands/Manager/ICommandManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ public interface ICommandManager : IEnumerable<IRichCommand>
8989
IRichCommand DecompressArchiveHereSmart { get; }
9090
IRichCommand DecompressArchiveToChildFolder { get; }
9191

92+
IRichCommand FlattenFolder { get; }
93+
9294
IRichCommand RotateLeft { get; }
9395
IRichCommand RotateRight { get; }
9496

src/Files.App/Data/Contracts/IGeneralSettingsService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
225225
/// </summary>
226226
bool ShowCompressionOptions { get; set; }
227227

228+
/// <summary>
229+
/// Gets or sets a value indicating whether or not to show the flatten options e.g. single, recursive.
230+
/// </summary>
231+
bool ShowFlattenOptions { get; set; }
232+
228233
/// <summary>
229234
/// Gets or sets a value indicating whether or not to show the Send To menu.
230235
/// </summary>

src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
547547
],
548548
ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && StorageArchiveService.CanDecompress(selectedItems)
549549
},
550+
new ContextMenuFlyoutItemViewModelBuilder(Commands.FlattenFolder).Build(),
550551
new ContextMenuFlyoutItemViewModel()
551552
{
552553
Text = "SendTo".GetLocalizedResource(),
@@ -587,7 +588,7 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
587588
ShowItem = isDriveRoot,
588589
IsEnabled = false
589590
},
590-
new ContextMenuFlyoutItemViewModelBuilder(Commands.EditInNotepad).Build(),
591+
new ContextMenuFlyoutItemViewModelBuilder(Commands.EditInNotepad).Build(),
591592
new ContextMenuFlyoutItemViewModel()
592593
{
593594
ItemType = ContextMenuFlyoutItemType.Separator,

src/Files.App/Services/Settings/GeneralSettingsService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,12 @@ public bool ShowCompressionOptions
239239
set => Set(value);
240240
}
241241

242+
public bool ShowFlattenOptions
243+
{
244+
get => Get(false);
245+
set => Set(value);
246+
}
247+
242248
public bool ShowSendToMenu
243249
{
244250
get => Get(true);

src/Files.App/Strings/en-US/Resources.resw

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1853,6 +1853,9 @@
18531853
<data name="SettingsSetAsDefaultFileManagerDescription" xml:space="preserve">
18541854
<value>This option modifies the system registry and can have unexpected side effects on your device. Continue at your own risk.</value>
18551855
</data>
1856+
<data name="ShowFlattenOptionsDescription" xml:space="preserve">
1857+
<value>The flatten operations are permanent and not recommended. Continue at your own risk.</value>
1858+
</data>
18561859
<data name="FolderWidgetCreateNewLibraryDialogTitleText" xml:space="preserve">
18571860
<value>Create Library</value>
18581861
</data>
@@ -2027,6 +2030,21 @@
20272030
<data name="Compress" xml:space="preserve">
20282031
<value>Compress</value>
20292032
</data>
2033+
<data name="FlattenFolderDescription" xml:space="preserve">
2034+
<value>Move all contents from subfolders into the selected location</value>
2035+
</data>
2036+
<data name="FlattenFolder" xml:space="preserve">
2037+
<value>Flatten folder</value>
2038+
</data>
2039+
<data name="Flatten" xml:space="preserve">
2040+
<value>Flatten</value>
2041+
</data>
2042+
<data name="FlattenFolderDialogContent" xml:space="preserve">
2043+
<value>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.</value>
2044+
</data>
2045+
<data name="ShowFlattenOptions" xml:space="preserve">
2046+
<value>Show flatten options</value>
2047+
</data>
20302048
<data name="SelectFilesAndFoldersOnHover" xml:space="preserve">
20312049
<value>Select files and folders when hovering over them</value>
20322050
</data>

src/Files.App/ViewModels/Settings/AdvancedViewModel.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,20 @@ public bool ShowSystemTrayIcon
348348
}
349349
}
350350

351+
// TODO remove when feature is marked as stable
352+
public bool ShowFlattenOptions
353+
{
354+
get => UserSettingsService.GeneralSettingsService.ShowFlattenOptions;
355+
set
356+
{
357+
if (value == UserSettingsService.GeneralSettingsService.ShowFlattenOptions)
358+
return;
359+
360+
UserSettingsService.GeneralSettingsService.ShowFlattenOptions = value;
361+
OnPropertyChanged();
362+
}
363+
}
364+
351365
public async Task OpenFilesOnWindowsStartupAsync()
352366
{
353367
var stateMode = await ReadState();

src/Files.App/ViewModels/Settings/GeneralViewModel.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,20 @@ public bool ShowCompressionOptions
454454
}
455455
}
456456

457+
// TODO uncomment code when feature is marked as stable
458+
//public bool ShowFlattenOptions
459+
//{
460+
// get => UserSettingsService.GeneralSettingsService.ShowFlattenOptions;
461+
// set
462+
// {
463+
// if (value == UserSettingsService.GeneralSettingsService.ShowFlattenOptions)
464+
// return;
465+
466+
// UserSettingsService.GeneralSettingsService.ShowFlattenOptions = value;
467+
// OnPropertyChanged();
468+
// }
469+
//}
470+
457471
public bool ShowSendToMenu
458472
{
459473
get => UserSettingsService.GeneralSettingsService.ShowSendToMenu;

0 commit comments

Comments
 (0)