Skip to content

Commit

Permalink
Feature: Added support for creating alternate data streams (files-com…
Browse files Browse the repository at this point in the history
  • Loading branch information
yaira2 authored Nov 6, 2024
1 parent 3214f89 commit af6207e
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 6 deletions.
116 changes: 116 additions & 0 deletions src/Files.App/Actions/FileSystem/CreateAlternateDataStreamAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) 2024 Files Community
// Licensed under the MIT License. See the LICENSE.

using Microsoft.UI.Xaml.Controls;
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>();
private static readonly IApplicationSettingsService ApplicationSettingsService = Ioc.Default.GetRequiredService<IApplicationSettingsService>();

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

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

public override bool IsExecutable =>
context.HasSelection &&
context.CanCreateItem &&
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 if (ApplicationSettingsService.ShowDataStreamsAreHiddenPrompt)
{
var dialog = new ContentDialog
{
Title = Strings.DataStreamsAreHiddenTitle.GetLocalizedResource(),
Content = Strings.DataStreamsAreHiddenDescription.GetLocalizedResource(),
PrimaryButtonText = Strings.Yes.GetLocalizedResource(),
SecondaryButtonText = Strings.DontShowAgain.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();
}
else
ApplicationSettingsService.ShowDataStreamsAreHiddenPrompt = false;
}
}

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/IApplicationSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,10 @@ public interface IApplicationSettingsService : IBaseSettingsService
/// </summary>
bool ShowRunningAsAdminPrompt { get; set; }

/// <summary>
/// Gets or sets a value indicating whether or not to display a prompt when creating an alternate data stream.
/// </summary>
bool ShowDataStreamsAreHiddenPrompt { get; set; }

}
}
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/ApplicationSettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ public bool ShowRunningAsAdminPrompt
get => Get(true);
set => Set(value);
}

public bool ShowDataStreamsAreHiddenPrompt
{
get => Get(true);
set => Set(value);
}

public ApplicationSettingsService(ISettingsSharingContext settingsSharingContext)
{
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 @@ -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

0 comments on commit af6207e

Please sign in to comment.