Skip to content

Commit af6207e

Browse files
authored
Feature: Added support for creating alternate data streams (#16438)
1 parent 3214f89 commit af6207e

File tree

14 files changed

+267
-6
lines changed

14 files changed

+267
-6
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2024 Files Community
2+
// Licensed under the MIT License. See the LICENSE.
3+
4+
using Microsoft.UI.Xaml.Controls;
5+
using Windows.Foundation.Metadata;
6+
7+
namespace Files.App.Actions
8+
{
9+
internal sealed class CreateAlternateDataStreamAction : BaseUIAction, IAction
10+
{
11+
private readonly IContentPageContext context;
12+
13+
private static readonly IFoldersSettingsService FoldersSettingsService = Ioc.Default.GetRequiredService<IFoldersSettingsService>();
14+
private static readonly IApplicationSettingsService ApplicationSettingsService = Ioc.Default.GetRequiredService<IApplicationSettingsService>();
15+
16+
public string Label
17+
=> Strings.CreateAlternateDataStream.GetLocalizedResource();
18+
19+
public string Description
20+
=> Strings.CreateAlternateDataStreamDescription.GetLocalizedResource();
21+
22+
public override bool IsExecutable =>
23+
context.HasSelection &&
24+
context.CanCreateItem &&
25+
UIHelpers.CanShowDialog;
26+
27+
public CreateAlternateDataStreamAction()
28+
{
29+
context = Ioc.Default.GetRequiredService<IContentPageContext>();
30+
31+
context.PropertyChanged += Context_PropertyChanged;
32+
}
33+
34+
public async Task ExecuteAsync(object? parameter = null)
35+
{
36+
var nameDialog = DynamicDialogFactory.GetFor_CreateAlternateDataStreamDialog();
37+
await nameDialog.TryShowAsync();
38+
39+
if (nameDialog.DynamicResult != DynamicDialogResult.Primary)
40+
return;
41+
42+
var userInput = nameDialog.ViewModel.AdditionalData as string;
43+
await Task.WhenAll(context.SelectedItems.Select(async selectedItem =>
44+
{
45+
var isDateOk = Win32Helper.GetFileDateModified(selectedItem.ItemPath, out var dateModified);
46+
var isReadOnly = Win32Helper.HasFileAttribute(selectedItem.ItemPath, System.IO.FileAttributes.ReadOnly);
47+
48+
// Unset read-only attribute (#7534)
49+
if (isReadOnly)
50+
Win32Helper.UnsetFileAttribute(selectedItem.ItemPath, System.IO.FileAttributes.ReadOnly);
51+
52+
if (!Win32Helper.WriteStringToFile($"{selectedItem.ItemPath}:{userInput}", ""))
53+
{
54+
var dialog = new ContentDialog
55+
{
56+
Title = Strings.ErrorCreatingDataStreamTitle.GetLocalizedResource(),
57+
Content = Strings.ErrorCreatingDataStreamDescription.GetLocalizedResource(),
58+
PrimaryButtonText = "Ok".GetLocalizedResource()
59+
};
60+
61+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
62+
dialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
63+
64+
await dialog.TryShowAsync();
65+
}
66+
67+
// Restore read-only attribute (#7534)
68+
if (isReadOnly)
69+
Win32Helper.SetFileAttribute(selectedItem.ItemPath, System.IO.FileAttributes.ReadOnly);
70+
71+
// Restore date modified
72+
if (isDateOk)
73+
Win32Helper.SetFileDateModified(selectedItem.ItemPath, dateModified);
74+
}));
75+
76+
if (context.ShellPage is null)
77+
return;
78+
79+
if (FoldersSettingsService.AreAlternateStreamsVisible)
80+
await context.ShellPage.Refresh_Click();
81+
else if (ApplicationSettingsService.ShowDataStreamsAreHiddenPrompt)
82+
{
83+
var dialog = new ContentDialog
84+
{
85+
Title = Strings.DataStreamsAreHiddenTitle.GetLocalizedResource(),
86+
Content = Strings.DataStreamsAreHiddenDescription.GetLocalizedResource(),
87+
PrimaryButtonText = Strings.Yes.GetLocalizedResource(),
88+
SecondaryButtonText = Strings.DontShowAgain.GetLocalizedResource()
89+
};
90+
91+
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
92+
dialog.XamlRoot = MainWindow.Instance.Content.XamlRoot;
93+
94+
var result = await dialog.TryShowAsync();
95+
if (result == ContentDialogResult.Primary)
96+
{
97+
FoldersSettingsService.AreAlternateStreamsVisible = true;
98+
await context.ShellPage.Refresh_Click();
99+
}
100+
else
101+
ApplicationSettingsService.ShowDataStreamsAreHiddenPrompt = false;
102+
}
103+
}
104+
105+
private void Context_PropertyChanged(object? sender, PropertyChangedEventArgs e)
106+
{
107+
switch (e.PropertyName)
108+
{
109+
case nameof(IContentPageContext.HasSelection):
110+
case nameof(IContentPageContext.CanCreateItem):
111+
OnPropertyChanged(nameof(IsExecutable));
112+
break;
113+
}
114+
}
115+
}
116+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public enum CommandCodes
4242
CreateFolder,
4343
CreateFolderWithSelection,
4444
AddItem,
45+
CreateAlternateDataStream,
4546
CreateShortcut,
4647
CreateShortcutFromDialog,
4748
EmptyRecycleBin,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public IRichCommand this[HotKey hotKey]
6767
public IRichCommand RestoreAllRecycleBin => commands[CommandCodes.RestoreAllRecycleBin];
6868
public IRichCommand RefreshItems => commands[CommandCodes.RefreshItems];
6969
public IRichCommand Rename => commands[CommandCodes.Rename];
70+
public IRichCommand CreateAlternateDataStream => commands[CommandCodes.CreateAlternateDataStream];
7071
public IRichCommand CreateShortcut => commands[CommandCodes.CreateShortcut];
7172
public IRichCommand CreateShortcutFromDialog => commands[CommandCodes.CreateShortcutFromDialog];
7273
public IRichCommand CreateFolder => commands[CommandCodes.CreateFolder];
@@ -263,6 +264,7 @@ public IEnumerator<IRichCommand> GetEnumerator() =>
263264
[CommandCodes.RestoreAllRecycleBin] = new RestoreAllRecycleBinAction(),
264265
[CommandCodes.RefreshItems] = new RefreshItemsAction(),
265266
[CommandCodes.Rename] = new RenameAction(),
267+
[CommandCodes.CreateAlternateDataStream] = new CreateAlternateDataStreamAction(),
266268
[CommandCodes.CreateShortcut] = new CreateShortcutAction(),
267269
[CommandCodes.CreateShortcutFromDialog] = new CreateShortcutFromDialogAction(),
268270
[CommandCodes.CreateFolder] = new CreateFolderAction(),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ public interface ICommandManager : IEnumerable<IRichCommand>
5151
IRichCommand CreateFolder { get; }
5252
IRichCommand CreateFolderWithSelection { get; }
5353
IRichCommand AddItem { get; }
54+
IRichCommand CreateAlternateDataStream { get; }
5455
IRichCommand CreateShortcut { get; }
5556
IRichCommand CreateShortcutFromDialog { get; }
5657
IRichCommand EmptyRecycleBin { get; }

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,10 @@ public interface IApplicationSettingsService : IBaseSettingsService
1515
/// </summary>
1616
bool ShowRunningAsAdminPrompt { get; set; }
1717

18+
/// <summary>
19+
/// Gets or sets a value indicating whether or not to display a prompt when creating an alternate data stream.
20+
/// </summary>
21+
bool ShowDataStreamsAreHiddenPrompt { get; set; }
22+
1823
}
1924
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ public interface IGeneralSettingsService : IBaseSettingsService, INotifyProperty
210210
/// </summary>
211211
bool ShowCopyPath { get; set; }
212212

213+
/// <summary>
214+
/// Gets or sets a value indicating whether or not to show the option to create alternate data stream.
215+
/// </summary>
216+
bool ShowCreateAlternateDataStream { get; set; }
217+
213218
/// <summary>
214219
/// Gets or sets a value indicating whether or not to show the option to create a shortcut.
215220
/// </summary>

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,11 @@ public static List<ContextMenuFlyoutItemViewModel> GetBaseItemMenuItems(
477477
&& (!selectedItems.FirstOrDefault()?.IsShortcut ?? false)
478478
&& !currentInstanceViewModel.IsPageTypeRecycleBin,
479479
}.Build(),
480+
new ContextMenuFlyoutItemViewModelBuilder(Commands.CreateAlternateDataStream)
481+
{
482+
IsVisible = UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream &&
483+
Commands.CreateAlternateDataStream.IsExecutable,
484+
}.Build(),
480485
new ContextMenuFlyoutItemViewModelBuilder(Commands.Rename)
481486
{
482487
IsPrimary = true,

src/Files.App/Helpers/Dialog/DialogDisplayHelper.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
// Licensed under the MIT License. See the LICENSE.
33

44
using Files.App.Dialogs;
5-
using Files.App.ViewModels.Dialogs;
65
using Microsoft.UI.Xaml.Controls;
7-
using System;
8-
using System.Threading.Tasks;
96

107
namespace Files.App.Helpers
118
{

src/Files.App/Helpers/Dialog/DynamicDialogFactory.cs

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,11 @@
22
// Licensed under the MIT License. See the LICENSE.
33

44
using Files.App.Dialogs;
5-
using Files.App.ViewModels.Dialogs;
65
using Microsoft.UI;
7-
using Microsoft.UI.Text;
86
using Microsoft.UI.Xaml;
97
using Microsoft.UI.Xaml.Controls;
108
using Microsoft.UI.Xaml.Data;
119
using Microsoft.UI.Xaml.Media;
12-
using Windows.ApplicationModel.DataTransfer;
1310
using Windows.System;
1411

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

334331
return dialog;
335332
}
333+
334+
public static DynamicDialog GetFor_CreateAlternateDataStreamDialog()
335+
{
336+
DynamicDialog? dialog = null;
337+
TextBox inputText = new()
338+
{
339+
PlaceholderText = Strings.EnterDataStreamName.GetLocalizedResource()
340+
};
341+
342+
TeachingTip warning = new()
343+
{
344+
Title = Strings.InvalidFilename_Text.GetLocalizedResource(),
345+
PreferredPlacement = TeachingTipPlacementMode.Bottom,
346+
DataContext = new CreateItemDialogViewModel(),
347+
};
348+
349+
warning.SetBinding(TeachingTip.TargetProperty, new Binding()
350+
{
351+
Source = inputText
352+
});
353+
warning.SetBinding(TeachingTip.IsOpenProperty, new Binding()
354+
{
355+
Mode = BindingMode.OneWay,
356+
Path = new PropertyPath("IsNameInvalid")
357+
});
358+
359+
inputText.Resources.Add("InvalidNameWarningTip", warning);
360+
361+
inputText.TextChanged += (textBox, args) =>
362+
{
363+
var isInputValid = FilesystemHelpers.IsValidForFilename(inputText.Text);
364+
((CreateItemDialogViewModel)warning.DataContext).IsNameInvalid = !string.IsNullOrEmpty(inputText.Text) && !isInputValid;
365+
dialog!.ViewModel.DynamicButtonsEnabled = isInputValid
366+
? DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel
367+
: DynamicDialogButtons.Cancel;
368+
if (isInputValid)
369+
dialog.ViewModel.AdditionalData = inputText.Text;
370+
};
371+
372+
inputText.Loaded += (s, e) =>
373+
{
374+
// dispatching to the ui thread fixes an issue where the primary dialog button would steal focus
375+
_ = inputText.DispatcherQueue.EnqueueOrInvokeAsync(() => inputText.Focus(FocusState.Programmatic));
376+
};
377+
378+
dialog = new DynamicDialog(new DynamicDialogViewModel()
379+
{
380+
TitleText = string.Format(Strings.CreateAlternateDataStream.GetLocalizedResource()),
381+
SubtitleText = null,
382+
DisplayControl = new Grid()
383+
{
384+
MinWidth = 300d,
385+
Children =
386+
{
387+
inputText
388+
}
389+
},
390+
PrimaryButtonAction = (vm, e) =>
391+
{
392+
vm.HideDialog();
393+
},
394+
PrimaryButtonText = Strings.Create.GetLocalizedResource(),
395+
CloseButtonText = Strings.Cancel.GetLocalizedResource(),
396+
DynamicButtonsEnabled = DynamicDialogButtons.Cancel,
397+
DynamicButtons = DynamicDialogButtons.Primary | DynamicDialogButtons.Cancel
398+
});
399+
400+
dialog.Closing += (s, e) =>
401+
{
402+
warning.IsOpen = false;
403+
};
404+
405+
return dialog;
406+
}
336407
}
337408
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ public bool ShowRunningAsAdminPrompt
1919
get => Get(true);
2020
set => Set(value);
2121
}
22+
23+
public bool ShowDataStreamsAreHiddenPrompt
24+
{
25+
get => Get(true);
26+
set => Set(value);
27+
}
2228

2329
public ApplicationSettingsService(ISettingsSharingContext settingsSharingContext)
2430
{

0 commit comments

Comments
 (0)