Skip to content

Feature: Add a UI to edit, delete and create tags #11249

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d5df613
Feature: Add a UI to edit, delete and create tags
ferrariofilippo Feb 11, 2023
36829f2
Name Edit
ferrariofilippo Feb 11, 2023
bf3e680
Color picker
ferrariofilippo Feb 11, 2023
523ffdb
Fixed UI Position
ferrariofilippo Feb 11, 2023
400d5e1
Tags
ferrariofilippo Feb 11, 2023
8411c2b
Crash Fix
ferrariofilippo Feb 11, 2023
baa7caa
Tab Selection
ferrariofilippo Feb 11, 2023
917c62a
Small Optimization
ferrariofilippo Feb 11, 2023
f5bcadd
Requested Changes
ferrariofilippo Feb 12, 2023
96abbcd
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
ferrariofilippo Feb 12, 2023
de9b29d
Missing String
ferrariofilippo Feb 12, 2023
cb27b6d
Context Menu Tags Colors
ferrariofilippo Feb 12, 2023
7be4a03
Rename using F2
ferrariofilippo Feb 12, 2023
93b3ba6
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
ferrariofilippo Feb 12, 2023
21a2dff
Requested Changes
ferrariofilippo Feb 12, 2023
c0f8688
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
ferrariofilippo Feb 12, 2023
7b5aa49
Update Advanced.xaml
yaira2 Feb 12, 2023
6788dc5
XAML
yaira2 Feb 12, 2023
6501cd7
XAML
yaira2 Feb 12, 2023
ac9a0f7
Visibility Binding
ferrariofilippo Feb 12, 2023
eeca37b
Update Advanced.xaml
yaira2 Feb 12, 2023
0c32b1b
UX Improvement
ferrariofilippo Feb 13, 2023
03ddcc2
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
ferrariofilippo Feb 13, 2023
ca3a63d
Removed redundant saving and bug fix
ferrariofilippo Feb 13, 2023
cb14c83
Cancel Tag Edit
ferrariofilippo Feb 13, 2023
04699ea
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
yaira2 Feb 13, 2023
98d4f0a
Added cancel button
yaira2 Feb 13, 2023
12f017f
Update Advanced.xaml
yaira2 Feb 13, 2023
a3f2524
Update Advanced.xaml
yaira2 Feb 13, 2023
2231446
Reset Color
ferrariofilippo Feb 13, 2023
54ca65e
Update Sidebar
ferrariofilippo Feb 13, 2023
76f0819
Bug fixed
ferrariofilippo Feb 13, 2023
ebdbf1b
IsNameValid
yaira2 Feb 13, 2023
68d1fe6
Update Advanced.xaml.cs
yaira2 Feb 13, 2023
6c75e91
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
yaira2 Feb 13, 2023
6e0d1de
Merge branch 'main' into UI_To_Create_Edit_Delete_Tags
ferrariofilippo Feb 14, 2023
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
15 changes: 15 additions & 0 deletions src/Files.App/Filesystem/FileTagsManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Files.App.DataModels.NavigationControlItems;
using Files.Backend.Services.Settings;
using Files.Shared;
Expand Down Expand Up @@ -29,6 +30,20 @@ public IReadOnlyList<FileTagItem> FileTags
}
}

public FileTagsManager()
{
fileTagsSettingsService.OnTagsUpdated += TagsUpdated;
}

private async void TagsUpdated(object? _, EventArgs e)
{
lock (fileTags)
fileTags.Clear();
DataChanged?.Invoke(SectionType.FileTag, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));

await UpdateFileTagsAsync();
}

public Task UpdateFileTagsAsync()
{
try
Expand Down
28 changes: 24 additions & 4 deletions src/Files.App/Helpers/ColorHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using CommunityToolkit.WinUI.Helpers;
using System;
using Windows.UI;

Expand All @@ -11,11 +12,15 @@ internal static class ColorHelpers
public static Color FromHex(string colorHex)
{
colorHex = colorHex.Replace("#", string.Empty);
var r = (byte)Convert.ToUInt32(colorHex.Substring(0, 2), 16);
var g = (byte)Convert.ToUInt32(colorHex.Substring(2, 2), 16);
var b = (byte)Convert.ToUInt32(colorHex.Substring(4, 2), 16);

return Color.FromArgb(255, r, g, b);
var alphaOffset = colorHex.Length == 8 ? 2 : 0;

var a = alphaOffset == 2 ? (byte)Convert.ToUInt32(colorHex.Substring(0, 2), 16) : (byte)255;
var r = (byte)Convert.ToUInt32(colorHex.Substring(alphaOffset, 2), 16);
var g = (byte)Convert.ToUInt32(colorHex.Substring(alphaOffset + 2, 2), 16);
var b = (byte)Convert.ToUInt32(colorHex.Substring(alphaOffset + 4, 2), 16);

return Color.FromArgb(a, r, g, b);
}

/// <summary>
Expand All @@ -36,5 +41,20 @@ public static uint ToUint(this Color c)
{
return (uint)(((c.A << 24) | (c.R << 16) | (c.G << 8) | c.B) & 0xffffffffL);
}

/// <summary>
/// Generates a random color and returns its Hex
/// </summary>
/// <returns></returns>
public static string RandomColor()
{
var color = Color.FromArgb(
255,
(byte)Random.Shared.Next(0, 256),
(byte)Random.Shared.Next(0, 256),
(byte)Random.Shared.Next(0, 256));

return color.ToHex();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Files.App.Extensions;
using Files.App.Helpers;
using Files.App.Serialization;
using Files.App.Serialization.Implementation;
using Files.Backend.Services.Settings;
Expand All @@ -15,6 +16,8 @@ internal sealed class FileTagsSettingsService : BaseJsonSettings, IFileTagsSetti
{
public event EventHandler OnSettingImportedEvent;

public event EventHandler OnTagsUpdated;

private static readonly List<TagViewModel> DefaultFileTags = new List<TagViewModel>()
{
new("Home", "#0072BD", "f7e0e137-2eb5-4fa4-a50d-ddd65df17c34"),
Expand Down Expand Up @@ -73,6 +76,47 @@ public IEnumerable<TagViewModel> SearchTagsByName(string tagName)
return FileTagList.Where(x => x.Name.StartsWith(tagName, StringComparison.OrdinalIgnoreCase));
}

public void CreateNewTag(string newTagName, string color)
{
var newTag = new TagViewModel(
newTagName,
color,
Guid.NewGuid().ToString());

var oldTags = FileTagList.ToList();
oldTags.Add(newTag);
FileTagList = oldTags;
OnTagsUpdated.Invoke(this, EventArgs.Empty);
}

public void EditTag(string uid, string name, string color)
{
var (tag, index) = GetTagAndIndex(uid);
if (tag is null)
return;

tag.Name = name;
tag.Color = color;

var oldTags = FileTagList.ToList();
oldTags.RemoveAt(index);
oldTags.Insert(index, tag);
FileTagList = oldTags;
OnTagsUpdated.Invoke(this, EventArgs.Empty);
}

public void DeleteTag(string uid)
{
var (_, index) = GetTagAndIndex(uid);
if (index == -1)
return;

var oldTags = FileTagList.ToList();
oldTags.RemoveAt(index);
FileTagList = oldTags;
OnTagsUpdated.Invoke(this, EventArgs.Empty);
}

public override bool ImportSettings(object import)
{
if (import is string importString)
Expand Down Expand Up @@ -101,5 +145,23 @@ public override object ExportSettings()
// Return string in Json format
return JsonSettingsSerializer.SerializeToJson(FileTagList);
}

private (TagViewModel?, int) GetTagAndIndex(string uid)
{
TagViewModel? tag = null;
int index = -1;

for (int i = 0; i < FileTagList.Count; i++)
{
if (FileTagList[i].Uid == uid)
{
tag = FileTagList[i];
index = i;
break;
}
}

return (tag, index);
}
}
}
21 changes: 21 additions & 0 deletions src/Files.App/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -2931,6 +2931,27 @@
<data name="Never" xml:space="preserve">
<value>Never</value>
</data>
<data name="TagColor" xml:space="preserve">
<value>Tag color</value>
</data>
<data name="Rename" xml:space="preserve">
<value>Rename</value>
</data>
<data name="Commit" xml:space="preserve">
<value>Commit</value>
</data>
<data name="SaveChanges" xml:space="preserve">
<value>Save changes</value>
</data>
<data name="NewTag" xml:space="preserve">
<value>New tag</value>
</data>
<data name="CreateNewTag" xml:space="preserve">
<value>Create new tag</value>
</data>
<data name="LoadingMoreOptions" xml:space="preserve">
<value>Loading more options...</value>
</data>
<data name="Loading" xml:space="preserve">
<value>Loading...</value>
</data>
Expand Down
115 changes: 100 additions & 15 deletions src/Files.App/ViewModels/SettingsViewModels/AdvancedViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
using Files.App.Helpers;
using Files.App.Shell;
using Files.Backend.Services.Settings;
using Files.Backend.ViewModels.FileTags;
using Files.Shared.Extensions;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Win32;
using SevenZip;
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Text;
Expand All @@ -31,25 +34,37 @@ public class AdvancedViewModel : ObservableObject

private readonly IFileTagsSettingsService fileTagsSettingsService = Ioc.Default.GetRequiredService<IFileTagsSettingsService>();

public ICommand EditFileTagsCommand { get; }
public ICommand SetAsDefaultExplorerCommand { get; }
public ICommand SetAsOpenFileDialogCommand { get; }
public ICommand ExportSettingsCommand { get; }
public ICommand ImportSettingsCommand { get; }
public ICommand OpenSettingsJsonCommand { get; }
public ICommand AddTagCommand { get; }
public ICommand SaveNewTagCommand { get; }
public ICommand CancelNewTagCommand { get; }

public NewTagViewModel NewTag = new();

public ObservableCollection<ListedTagViewModel> Tags { get; set; }

public AdvancedViewModel()
{
IsSetAsDefaultFileManager = DetectIsSetAsDefaultFileManager();
IsSetAsOpenFileDialog = DetectIsSetAsOpenFileDialog();

EditFileTagsCommand = new AsyncRelayCommand(LaunchFileTagsConfigFile);
SetAsDefaultExplorerCommand = new AsyncRelayCommand(SetAsDefaultExplorer);
SetAsOpenFileDialogCommand = new AsyncRelayCommand(SetAsOpenFileDialog);
ExportSettingsCommand = new AsyncRelayCommand(ExportSettings);
ImportSettingsCommand = new AsyncRelayCommand(ImportSettings);
OpenSettingsJsonCommand = new AsyncRelayCommand(OpenSettingsJson);

// Tags Commands
AddTagCommand = new RelayCommand(DoAddNewTag);
SaveNewTagCommand = new RelayCommand(DoSaveNewTag);
CancelNewTagCommand = new RelayCommand(DoCancelNewTag);

Tags = new ObservableCollection<ListedTagViewModel>();
fileTagsSettingsService.FileTagList?.ForEach(tag => Tags.Add(new ListedTagViewModel(tag)));
}

private async Task OpenSettingsJson()
Expand All @@ -62,16 +77,6 @@ private async Task OpenSettingsJson()
}
}

private async Task LaunchFileTagsConfigFile()
{
var configFile = await StorageFile.GetFileFromApplicationUriAsync(new Uri("ms-appdata:///local/settings/filetags.json"));

if (!await Launcher.LaunchFileAsync(configFile))
{
await ContextMenu.InvokeVerb("open", configFile.Path);
}
}

private async Task SetAsDefaultExplorer()
{
// Make sure IsSetAsDefaultFileManager is updated
Expand All @@ -88,7 +93,8 @@ private async Task SetAsDefaultExplorer()
if (!SafetyExtensions.IgnoreExceptions(() => File.Copy(file, Path.Combine(destFolder, Path.GetFileName(file)), true), App.Logger))
{
// Error copying files
goto DetectResult;
await DetectResult();
return;
}
}

Expand All @@ -98,7 +104,8 @@ private async Task SetAsDefaultExplorer()
if (!Win32API.RunPowershellCommand($"-command \"New-Item -Force -Path '{dataPath}' -ItemType Directory; Copy-Item -Filter *.* -Path '{destFolder}\\*' -Recurse -Force -Destination '{dataPath}'\"", false))
{
// Error copying files
goto DetectResult;
await DetectResult();
return;
}
}
else
Expand All @@ -116,7 +123,11 @@ private async Task SetAsDefaultExplorer()
// Canceled UAC
}

DetectResult:
await DetectResult();
}

private async Task DetectResult()
{
IsSetAsDefaultFileManager = DetectIsSetAsDefaultFileManager();
if (!IsSetAsDefaultFileManager)
{
Expand Down Expand Up @@ -286,6 +297,13 @@ public bool IsSetAsOpenFileDialog
set => SetProperty(ref isSetAsOpenFileDialog, value);
}

private bool isCreatingNewTag;
public bool IsCreatingNewTag
{
get => isCreatingNewTag;
set => SetProperty(ref isCreatingNewTag, value);
}

private FileSavePicker InitializeWithWindow(FileSavePicker obj)
{
WinRT.Interop.InitializeWithWindow.Initialize(obj, App.WindowHandle);
Expand All @@ -299,5 +317,72 @@ private FileOpenPicker InitializeWithWindow(FileOpenPicker obj)

return obj;
}

private void DoAddNewTag()
{
NewTag.Reset();
IsCreatingNewTag = true;
}

private void DoSaveNewTag()
{
IsCreatingNewTag = false;

fileTagsSettingsService.CreateNewTag(NewTag.Name, NewTag.Color);
Tags.Clear();
fileTagsSettingsService.FileTagList?.ForEach(tag => Tags.Add(new ListedTagViewModel(tag)));
}

private void DoCancelNewTag()
{
IsCreatingNewTag = false;
}

public void EditExistingTag(ListedTagViewModel item, string newName, string color)
{
fileTagsSettingsService.EditTag(item.Tag.Uid, newName, color);
Tags.Clear();
fileTagsSettingsService.FileTagList?.ForEach(tag => Tags.Add(new ListedTagViewModel(tag)));
}

public void DeleteExistingTag(ListedTagViewModel item)
{
Tags.Remove(item);
fileTagsSettingsService.DeleteTag(item.Tag.Uid);
}
}

public class NewTagViewModel : ObservableObject
{
private string name = string.Empty;
public string Name
{
get => name;
set
{
if (SetProperty(ref name, value))
OnPropertyChanged(nameof(IsNameValid));
}
}

private string color = "#FFFFFFFF";
public string Color
{
get => color;
set => SetProperty(ref color, value);
}

private bool isNameValid;
public bool IsNameValid
{
get => isNameValid;
set => SetProperty(ref isNameValid, value);
}

public void Reset()
{
Name = string.Empty;
Color = ColorHelpers.RandomColor();
}
}
}
Loading