Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Added experimental support for flattening folders #15992

Merged
merged 28 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
250ad3e
Feature: Add support for flattening folders
devin-slothower Aug 11, 2024
e8f9117
Merge branch 'files-community:main' into main
devin-slothower Aug 11, 2024
1676208
Add resources to be translated
devin-slothower Aug 11, 2024
6d2403f
Merge branch 'main' of https://github.com/devin-slothower/Files
devin-slothower Aug 11, 2024
dbb54bf
Copyright header
devin-slothower Aug 11, 2024
386836b
Delete src/Files.App/Properties/Resources.Designer.cs
devin-slothower Aug 11, 2024
99041e8
Delete src/Files.App/Properties/Resources.resx
devin-slothower Aug 11, 2024
155e20d
Revert changes to resource files
yaira2 Aug 12, 2024
b1846c4
Merge branch 'files-community:main' into main
devin-slothower Aug 12, 2024
8e957ca
Code Review: Changed to synchronous execution
devin-slothower Aug 12, 2024
1937828
Code Review: Flatten option defaulted to false
devin-slothower Aug 12, 2024
b964919
Code Review: Flyout Icon
devin-slothower Aug 13, 2024
f0e9fd2
Cleanup
yaira2 Aug 26, 2024
bf9b68a
Update CommandManager.cs
yaira2 Aug 26, 2024
4cf6ce5
Update Resources.resw
yaira2 Aug 26, 2024
87ec819
Update CommandCodes.cs
yaira2 Aug 26, 2024
108bf27
Rename
yaira2 Aug 26, 2024
a63a797
Update Resources.resw
yaira2 Aug 26, 2024
f6d5b54
Update src/Files.App/Actions/FileSystem/FlattenToRootAction.cs
yaira2 Aug 26, 2024
76b73a2
Update IsExecutable
yaira2 Aug 27, 2024
136dfa8
Merge actions
yaira2 Aug 27, 2024
182b1b4
Update FlattenToRootAction.cs
yaira2 Aug 27, 2024
8598d64
Update Resources.resw
yaira2 Aug 27, 2024
9c35da2
Update FlattenToRootAction.cs
yaira2 Aug 27, 2024
819473d
Update Resources.resw
yaira2 Aug 27, 2024
e0d7ec6
Strings
yaira2 Aug 27, 2024
52f55d8
Update FlattenFolderAction.cs
yaira2 Aug 27, 2024
96d6912
Update Resources.resw
yaira2 Aug 27, 2024
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
110 changes: 110 additions & 0 deletions src/Files.App/Actions/FileSystem/FlattenRecursiveAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright (c) 2024 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.IO;
using Windows.Storage;
using Microsoft.Extensions.Logging;

namespace Files.App.Actions
{
internal sealed class FlattenRecursiveAction : ObservableObject, IAction
{
private readonly IContentPageContext context;

public string Label
=> "FlattenRecursive".GetLocalizedResource();

public string Description
=> "FlattenDescription".GetLocalizedResource();

public bool IsExecutable =>
context.ShellPage is not null &&
context.HasSelection &&
context.SelectedItem?.PrimaryItemAttribute is StorageItemTypes.Folder;

public FlattenRecursiveAction()
{
context = Ioc.Default.GetRequiredService<IContentPageContext>();

context.PropertyChanged += Context_PropertyChanged;
}

public async Task ExecuteAsync(object? parameter = null)
{
if (context.ShellPage?.ShellViewModel is null)
return;

var items = context.SelectedItems;

if (items is null || !items.Any() || items.Any(item => !item.IsFolder))
return;

foreach (var item in items)
{
await FlattenFolderAsync(item.ItemPath);
}
}

private async Task FlattenFolderAsync(string folderPath)
{
var containedFolders = await Task.Run(() => Directory.GetDirectories(folderPath));
var containedFiles = await Task.Run(() => Directory.GetFiles(folderPath));
devin-slothower marked this conversation as resolved.
Show resolved Hide resolved

foreach (var containedFolder in containedFolders)
{
await FlattenFolderAsync(containedFolder);
devin-slothower marked this conversation as resolved.
Show resolved Hide resolved

var folderName = Path.GetFileName(containedFolder);
var destinationPath = Path.Combine(context.ShellPage?.ShellViewModel?.CurrentFolder?.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.ShellPage?.ShellViewModel?.CurrentFolder?.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(folderPath).Length == 0 && Directory.GetDirectories(folderPath).Length == 0)
{
try
{
Directory.Delete(folderPath);
}
catch (Exception ex)
{
App.Logger.LogWarning(ex.Message, $"Failed to delete folder '{folderPath}'.");
}
}
}

private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(IContentPageContext.HasSelection))
OnPropertyChanged(nameof(IsExecutable));
}
}
}
105 changes: 105 additions & 0 deletions src/Files.App/Actions/FileSystem/FlattenSingleAction.cs
devin-slothower marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright (c) 2024 Files Community
// Licensed under the MIT License. See the LICENSE.

using System.IO;
using Windows.Storage;
using Microsoft.Extensions.Logging;

namespace Files.App.Actions
{
internal sealed class FlattenSingleAction : ObservableObject, IAction
{
private readonly IContentPageContext context;

public string Label
=> "FlattenSingle".GetLocalizedResource();

public string Description
=> "FlattenDescription".GetLocalizedResource();

public bool IsExecutable =>
context.ShellPage is not null &&
context.HasSelection &&
context.SelectedItem?.PrimaryItemAttribute is StorageItemTypes.Folder;

public FlattenSingleAction()
{
context = Ioc.Default.GetRequiredService<IContentPageContext>();

context.PropertyChanged += Context_PropertyChanged;
}

public async Task ExecuteAsync(object? parameter = null)
{
if (context.ShellPage?.ShellViewModel is null)
return;

var items = context.SelectedItems;

if (items is null || !items.Any() || items.Any(item => !item.IsFolder))
return;

foreach (var item in items)
{
var folderPath = item.ItemPath;

var containedFolders = await Task.Run(() => Directory.GetDirectories(folderPath));
var containedFiles = await Task.Run(() => Directory.GetFiles(folderPath));

foreach (var containedFolder in containedFolders)
{
var folderName = Path.GetFileName(containedFolder);
var destinationPath = Path.Combine(context.ShellPage?.ShellViewModel?.CurrentFolder?.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.ShellPage?.ShellViewModel?.CurrentFolder?.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(folderPath).Length == 0 && Directory.GetDirectories(folderPath).Length == 0)
{
try
{
Directory.Delete(folderPath);
}
catch (Exception ex)
{
App.Logger.LogWarning(ex.Message, $"Failed to delete folder '{folderPath}'.");
}
}
}
}

private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(IContentPageContext.HasSelection))
OnPropertyChanged(nameof(IsExecutable));
}
}
}
4 changes: 4 additions & 0 deletions src/Files.App/Data/Commands/Manager/CommandCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ public enum CommandCodes
DecompressArchiveHereSmart,
DecompressArchiveToChildFolder,

// Folder
FlattenSingle,
FlattenRecursive,

// Image Manipulation
RotateLeft,
RotateRight,
Expand Down
4 changes: 4 additions & 0 deletions src/Files.App/Data/Commands/Manager/CommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ 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 FlattenSingle => commands[CommandCodes.FlattenSingle];
public IRichCommand FlattenRecursive => commands[CommandCodes.FlattenRecursive];
public IRichCommand RotateLeft => commands[CommandCodes.RotateLeft];
public IRichCommand RotateRight => commands[CommandCodes.RotateRight];
public IRichCommand OpenItem => commands[CommandCodes.OpenItem];
Expand Down Expand Up @@ -289,6 +291,8 @@ public IEnumerator<IRichCommand> GetEnumerator() =>
[CommandCodes.DecompressArchiveHere] = new DecompressArchiveHere(),
[CommandCodes.DecompressArchiveHereSmart] = new DecompressArchiveHereSmart(),
[CommandCodes.DecompressArchiveToChildFolder] = new DecompressArchiveToChildFolderAction(),
[CommandCodes.FlattenSingle] = new FlattenSingleAction(),
[CommandCodes.FlattenRecursive] = new FlattenRecursiveAction(),
[CommandCodes.RotateLeft] = new RotateLeftAction(),
[CommandCodes.RotateRight] = new RotateRightAction(),
[CommandCodes.OpenItem] = new OpenItemAction(),
Expand Down
3 changes: 3 additions & 0 deletions src/Files.App/Data/Commands/Manager/ICommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public interface ICommandManager : IEnumerable<IRichCommand>
IRichCommand DecompressArchiveHereSmart { get; }
IRichCommand DecompressArchiveToChildFolder { get; }

IRichCommand FlattenSingle { get; }
IRichCommand FlattenRecursive { get; }

IRichCommand RotateLeft { get; }
IRichCommand RotateRight { get; }

Expand Down
5 changes: 5 additions & 0 deletions src/Files.App/Data/Contracts/IGeneralSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
/// </summary>
bool ShowCompressionOptions { get; set; }

/// <summary>
/// Gets or sets a value indicating whether or not to show the flatten options e.g. single, recursive.
/// </summary>
bool ShowFlattenOptions { get; set; }

/// <summary>
/// Gets or sets a value indicating whether or not to show the Send To menu.
/// </summary>
Expand Down
16 changes: 16 additions & 0 deletions src/Files.App/Data/Factories/ContentPageContextFlyoutFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,22 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
ShowItem = UserSettingsService.GeneralSettingsService.ShowCompressionOptions && StorageArchiveService.CanDecompress(selectedItems)
},
new ContextMenuFlyoutItemViewModel()
{
Text = "Flatten".GetLocalizedResource(),
ShowInSearchPage = true,
ThemedIconModel = new ThemedIconModel()
{
ThemedIconStyle = "App.ThemedIcons.Folder",
},
devin-slothower marked this conversation as resolved.
Show resolved Hide resolved
Items =
[
new ContextMenuFlyoutItemViewModelBuilder(Commands.FlattenSingle).Build(),
new ContextMenuFlyoutItemViewModelBuilder(Commands.FlattenRecursive).Build(),
],
IsHidden = selectedItems.Count != 1 || !selectedItems.Any(item => item?.PrimaryItemAttribute is StorageItemTypes.Folder) || !itemsSelected,
ShowItem = UserSettingsService.GeneralSettingsService.ShowFlattenOptions
},
new ContextMenuFlyoutItemViewModel()
{
Text = "SendTo".GetLocalizedResource(),
Tag = "SendTo",
Expand Down
6 changes: 6 additions & 0 deletions src/Files.App/Services/Settings/GeneralSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,12 @@ public bool ShowSystemTrayIcon
set => Set(value);
}

public bool ShowFlattenOptions
{
get => Get(true);
devin-slothower marked this conversation as resolved.
Show resolved Hide resolved
set => Set(value);
}

public FileNameConflictResolveOptionType ConflictsResolveOption
{
get => (FileNameConflictResolveOptionType)Get((long)FileNameConflictResolveOptionType.GenerateNewName);
Expand Down
12 changes: 12 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2027,6 +2027,18 @@
<data name="Compress" xml:space="preserve">
<value>Compress</value>
</data>
<data name="Flatten" xml:space="preserve">
<value>Flatten</value>
</data>
<data name="FlattenDescription" xml:space="preserve">
<value>Flatten a folder contents into the current path.</value>
</data>
<data name="FlattenSingle" xml:space="preserve">
<value>Flatten Single</value>
</data>
yaira2 marked this conversation as resolved.
Show resolved Hide resolved
<data name="FlattenRecursive" xml:space="preserve">
<value>Flatten Recursive</value>
yaira2 marked this conversation as resolved.
Show resolved Hide resolved
</data>
<data name="SelectFilesAndFoldersOnHover" xml:space="preserve">
<value>Select files and folders when hovering over them</value>
</data>
Expand Down