Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
115 changes: 115 additions & 0 deletions src/Files.App/Actions/FileSystem/CreateAlternateDataStreamAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// Copyright (c) 2024 Files Community
// Licensed under the MIT License. See the LICENSE.

using Microsoft.UI.Xaml.Controls;
using System.IO;
using Windows.Foundation.Metadata;

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

private static readonly IFoldersSettingsService FoldersSettingsService = Ioc.Default.GetRequiredService<IFoldersSettingsService>();

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

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

public override bool IsExecutable =>
context.HasSelection &&
context.CanCreateItem &&
(context.ShellPage?.ShellViewModel.WorkingDirectory != Path.GetPathRoot(context.ShellPage?.ShellViewModel.WorkingDirectory)) &&
UIHelpers.CanShowDialog;

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

context.PropertyChanged += Context_PropertyChanged;
}

public async Task ExecuteAsync(object? parameter = null)
{
var nameDialog = DynamicDialogFactory.GetFor_CreateAlternateDataStreamDialog();
await nameDialog.TryShowAsync();

if (nameDialog.DynamicResult != DynamicDialogResult.Primary)
return;

var userInput = nameDialog.ViewModel.AdditionalData as string;
await Task.WhenAll(context.SelectedItems.Select(async selectedItem =>
{
var isDateOk = Win32Helper.GetFileDateModified(selectedItem.ItemPath, out var dateModified);
var isReadOnly = Win32Helper.HasFileAttribute(selectedItem.ItemPath, System.IO.FileAttributes.ReadOnly);

// Unset read-only attribute (#7534)
if (isReadOnly)
Win32Helper.UnsetFileAttribute(selectedItem.ItemPath, System.IO.FileAttributes.ReadOnly);

if (!Win32Helper.WriteStringToFile($"{selectedItem.ItemPath}:{userInput}", ""))
{
var dialog = new ContentDialog
{
Title = Strings.ErrorCreatingDataStreamTitle.GetLocalizedResource(),
Content = Strings.ErrorCreatingDataStreamDescription.GetLocalizedResource(),
PrimaryButtonText = "Ok".GetLocalizedResource()
};

if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
dialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;

await dialog.TryShowAsync();
}

// Restore read-only attribute (#7534)
if (isReadOnly)
Win32Helper.SetFileAttribute(selectedItem.ItemPath, System.IO.FileAttributes.ReadOnly);

// Restore date modified
if (isDateOk)
Win32Helper.SetFileDateModified(selectedItem.ItemPath, dateModified);
}));

if (context.ShellPage is null)
return;

if (FoldersSettingsService.AreAlternateStreamsVisible)
await context.ShellPage.Refresh_Click();
else
{
var dialog = new ContentDialog
{
Title = Strings.DataStreamsAreHiddenTitle.GetLocalizedResource(),
Content = Strings.DataStreamsAreHiddenDescription.GetLocalizedResource(),
PrimaryButtonText = Strings.Yes.GetLocalizedResource(),
SecondaryButtonText = Strings.No.GetLocalizedResource()
};

if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
dialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;

var result = await dialog.TryShowAsync();
if (result == ContentDialogResult.Primary)
{
FoldersSettingsService.AreAlternateStreamsVisible = true;
await context.ShellPage.Refresh_Click();
}
}
}

private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case nameof(IContentPageContext.HasSelection):
case nameof(IContentPageContext.CanCreateItem):
OnPropertyChanged(nameof(IsExecutable));
break;
}
}
}
}
1 change: 1 addition & 0 deletions src/Files.App/Data/Commands/Manager/CommandCodes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public enum CommandCodes
CreateFolder,
CreateFolderWithSelection,
AddItem,
CreateAlternateDataStream,
CreateShortcut,
CreateShortcutFromDialog,
EmptyRecycleBin,
Expand Down
2 changes: 2 additions & 0 deletions src/Files.App/Data/Commands/Manager/CommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public IRichCommand this[HotKey hotKey]
public IRichCommand RestoreAllRecycleBin => commands[CommandCodes.RestoreAllRecycleBin];
public IRichCommand RefreshItems => commands[CommandCodes.RefreshItems];
public IRichCommand Rename => commands[CommandCodes.Rename];
public IRichCommand CreateAlternateDataStream => commands[CommandCodes.CreateAlternateDataStream];
public IRichCommand CreateShortcut => commands[CommandCodes.CreateShortcut];
public IRichCommand CreateShortcutFromDialog => commands[CommandCodes.CreateShortcutFromDialog];
public IRichCommand CreateFolder => commands[CommandCodes.CreateFolder];
Expand Down Expand Up @@ -263,6 +264,7 @@ public IEnumerator<IRichCommand> GetEnumerator() =>
[CommandCodes.RestoreAllRecycleBin] = new RestoreAllRecycleBinAction(),
[CommandCodes.RefreshItems] = new RefreshItemsAction(),
[CommandCodes.Rename] = new RenameAction(),
[CommandCodes.CreateAlternateDataStream] = new CreateAlternateDataStreamAction(),
[CommandCodes.CreateShortcut] = new CreateShortcutAction(),
[CommandCodes.CreateShortcutFromDialog] = new CreateShortcutFromDialogAction(),
[CommandCodes.CreateFolder] = new CreateFolderAction(),
Expand Down
1 change: 1 addition & 0 deletions src/Files.App/Data/Commands/Manager/ICommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public interface ICommandManager : IEnumerable<IRichCommand>
IRichCommand CreateFolder { get; }
IRichCommand CreateFolderWithSelection { get; }
IRichCommand AddItem { get; }
IRichCommand CreateAlternateDataStream { get; }
IRichCommand CreateShortcut { get; }
IRichCommand CreateShortcutFromDialog { get; }
IRichCommand EmptyRecycleBin { 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 @@ -210,6 +210,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
/// </summary>
bool ShowCopyPath { get; set; }

/// <summary>
/// Gets or sets a value indicating whether or not to show the option to create alternate data stream.
/// </summary>
bool ShowCreateAlternateDataStream { get; set; }

/// <summary>
/// Gets or sets a value indicating whether or not to show the option to create a shortcut.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,11 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
&& (!selectedItems.FirstOrDefault()?.IsShortcut ?? false)
&& !currentInstanceViewModel.IsPageTypeRecycleBin,
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(Commands.CreateAlternateDataStream)
{
IsVisible = UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream &&
Commands.CreateAlternateDataStream.IsExecutable,
}.Build(),
new ContextMenuFlyoutItemViewModelBuilder(Commands.Rename)
{
IsPrimary = true,
Expand Down
3 changes: 0 additions & 3 deletions src/Files.App/Helpers/Dialog/DialogDisplayHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,7 @@
// Licensed under the MIT License. See the LICENSE.

using Files.App.Dialogs;
using Files.App.ViewModels.Dialogs;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;

namespace Files.App.Helpers
{
Expand Down
77 changes: 74 additions & 3 deletions src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@
// Licensed under the MIT License. See the LICENSE.

using Files.App.Dialogs;
using Files.App.ViewModels.Dialogs;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media;
using Windows.ApplicationModel.DataTransfer;
using Windows.System;

namespace Files.App.Helpers
Expand Down Expand Up @@ -333,5 +330,79 @@ public static DynamicDialog GetFor_RenameRequiresHigherPermissions(string path)

return dialog;
}

public static DynamicDialog GetFor_CreateAlternateDataStreamDialog()
{
DynamicDialog? dialog = null;
TextBox inputText = new()
{
PlaceholderText = Strings.EnterDataStreamName.GetLocalizedResource()
};

TeachingTip warning = new()
{
Title = Strings.InvalidFilename_Text.GetLocalizedResource(),
PreferredPlacement = TeachingTipPlacementMode.Bottom,
DataContext = new CreateItemDialogViewModel(),
};

warning.SetBinding(TeachingTip.TargetProperty, new Binding()
{
Source = inputText
});
warning.SetBinding(TeachingTip.IsOpenProperty, new Binding()
{
Mode = BindingMode.OneWay,
Path = new PropertyPath("IsNameInvalid")
});

inputText.Resources.Add("InvalidNameWarningTip", warning);

inputText.TextChanged += (textBox, args) =>
{
var isInputValid = FilesystemHelpers.IsValidForFilename(inputText.Text);
((CreateItemDialogViewModel)warning.DataContext).IsNameInvalid = !string.IsNullOrEmpty(inputText.Text) && !isInputValid;
dialog!.ViewModel.DynamicButtonsEnabled = isInputValid
? DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel
: DynamicDialogButtons.Cancel;
if (isInputValid)
dialog.ViewModel.AdditionalData = inputText.Text;
};

inputText.Loaded += (s, e) =>
{
// dispatching to the ui thread fixes an issue where the primary dialog button would steal focus
_ = inputText.DispatcherQueue.EnqueueOrInvokeAsync(() => inputText.Focus(FocusState.Programmatic));
};

dialog = new DynamicDialog(new DynamicDialogViewModel()
{
TitleText = string.Format(Strings.CreateAlternateDataStream.GetLocalizedResource()),
SubtitleText = null,
DisplayControl = new Grid()
{
MinWidth = 300d,
Children =
{
inputText
}
},
PrimaryButtonAction = (vm, e) =>
{
vm.HideDialog();
},
PrimaryButtonText = Strings.Create.GetLocalizedResource(),
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
DynamicButtonsEnabled = DynamicDialogButtons.Cancel,
DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel
});

dialog.Closing += (s, e) =>
{
warning.IsOpen = false;
};

return dialog;
}
}
}
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 @@ -281,6 +281,12 @@ public bool ShowCreateFolderWithSelection
set => Set(value);
}

public bool ShowCreateAlternateDataStream
{
get => Get(false);
set => Set(value);
}

public bool ShowCreateShortcut
{
get => Get(true);
Expand Down
24 changes: 24 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -3971,4 +3971,28 @@
<data name="BulkRename" xml:space="preserve">
<value>Bulk rename</value>
</data>
<data name="ShowCreateAlternateDataStream" xml:space="preserve">
<value>Show option to create alternate data stream</value>
</data>
<data name="CreateAlternateDataStream" xml:space="preserve">
<value>Create alternate data stream</value>
</data>
<data name="CreateAlternateDataStreamDescription" xml:space="preserve">
<value>Create alternate data stream for the selected item(s)</value>
</data>
<data name="EnterDataStreamName" xml:space="preserve">
<value>Enter data stream name</value>
</data>
<data name="ErrorCreatingDataStreamTitle" xml:space="preserve">
<value>There was an error creating the alternate data stream</value>
</data>
<data name="ErrorCreatingDataStreamDescription" xml:space="preserve">
<value>Please note that alternate data streams only work on drives formatted as NTFS.</value>
</data>
<data name="DataStreamsAreHiddenTitle" xml:space="preserve">
<value>Alternate data streams are currently hidden</value>
</data>
<data name="DataStreamsAreHiddenDescription" xml:space="preserve">
<value>Would you like to display alternate data streams? You can modify this setting anytime from the files and folders settings page.</value>
</data>
</root>
14 changes: 14 additions & 0 deletions src/Files.App/ViewModels/Settings/GeneralViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,20 @@ public bool ShowCopyPath
}
}

public bool ShowCreateAlternateDataStream
{
get => UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream;
set
{
if (value != UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream)
{
UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream = value;

OnPropertyChanged();
}
}
}

public bool ShowCreateShortcut
{
get => UserSettingsService.GeneralSettingsService.ShowCreateShortcut;
Expand Down
8 changes: 8 additions & 0 deletions src/Files.App/Views/Settings/GeneralPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,14 @@
Style="{StaticResource RightAlignedToggleSwitchStyle}" />
</local:SettingsBlockControl>

<!-- Create alternate data stream -->
<local:SettingsBlockControl Title="{helpers:ResourceString Name=ShowCreateAlternateDataStream}" HorizontalAlignment="Stretch">
<ToggleSwitch
AutomationProperties.Name="{helpers:ResourceString Name=ShowCreateAlternateDataStream}"
IsOn="{x:Bind ViewModel.ShowCreateAlternateDataStream, Mode=TwoWay}"
Style="{StaticResource RightAlignedToggleSwitchStyle}" />
</local:SettingsBlockControl>

<!-- Create shortcut -->
<local:SettingsBlockControl Title="{helpers:ResourceString Name=ShowCreateShortcut}" HorizontalAlignment="Stretch">
<ToggleSwitch
Expand Down