Skip to content

Commit ac460e5

Browse files
committed
POC
1 parent 566cc8c commit ac460e5

File tree

11 files changed

+229
-0
lines changed

11 files changed

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

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/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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,13 @@ 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+
&& itemsSelected
484+
&& !currentInstanceViewModel.IsPageTypeRecycleBin
485+
&& !isDriveRoot,
486+
}.Build(),
480487
new ContextMenuFlyoutItemViewModelBuilder(Commands.Rename)
481488
{
482489
IsPrimary = true,

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

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Microsoft.UI.Xaml.Controls;
1010
using Microsoft.UI.Xaml.Data;
1111
using Microsoft.UI.Xaml.Media;
12+
using TagLib.Ape;
1213
using Windows.ApplicationModel.DataTransfer;
1314
using Windows.System;
1415

@@ -333,5 +334,79 @@ public static DynamicDialog GetFor_RenameRequiresHigherPermissions(string path)
333334

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ public bool ShowCreateFolderWithSelection
281281
set => Set(value);
282282
}
283283

284+
public bool ShowCreateAlternateDataStream
285+
{
286+
get => Get(false);
287+
set => Set(value);
288+
}
289+
284290
public bool ShowCreateShortcut
285291
{
286292
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
@@ -3971,4 +3971,22 @@
39713971
<data name="BulkRename" xml:space="preserve">
39723972
<value>Bulk rename</value>
39733973
</data>
3974+
<data name="ShowCreateAlternateDataStream" xml:space="preserve">
3975+
<value>Show option to create alternate data stream</value>
3976+
</data>
3977+
<data name="CreateAlternateDataStream" xml:space="preserve">
3978+
<value>Create alternate data stream</value>
3979+
</data>
3980+
<data name="CreateAlternateDataStreamDescription" xml:space="preserve">
3981+
<value>Create alternate data stream for the selected item(s)</value>
3982+
</data>
3983+
<data name="EnterDataStreamName" xml:space="preserve">
3984+
<value>Enter data stream name</value>
3985+
</data>
3986+
<data name="ErrorCreatingDataStreamTitle" xml:space="preserve">
3987+
<value>There was an error creating the alternate data stream</value>
3988+
</data>
3989+
<data name="ErrorCreatingDataStreamDescription" xml:space="preserve">
3990+
<value>Please note that alternate data streams only work on drives formatted with NTFS. To resolve this issue, ensure that the drive or partition you are working on is formatted with NTFS.</value>
3991+
</data>
39743992
</root>

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,20 @@ public bool ShowCopyPath
279279
}
280280
}
281281

282+
public bool ShowCreateAlternateDataStream
283+
{
284+
get => UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream;
285+
set
286+
{
287+
if (value != UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream)
288+
{
289+
UserSettingsService.GeneralSettingsService.ShowCreateAlternateDataStream = value;
290+
291+
OnPropertyChanged();
292+
}
293+
}
294+
}
295+
282296
public bool ShowCreateShortcut
283297
{
284298
get => UserSettingsService.GeneralSettingsService.ShowCreateShortcut;

0 commit comments

Comments
 (0)