feat(lists): assign a colour to a sidebar folder

Tints the folder name and its guide line; picked from a swatch row in the folder settings.
This commit is contained in:
mika kuns
2026-09-30 10:12:41 +02:00
parent 8a31a67c74
commit bb92dc279f
15 changed files with 1257 additions and 9 deletions
@@ -16,6 +16,8 @@ public sealed partial class ListFolderNavItemViewModel : ViewModelBase
public string? ParentFolderId { get; set; }
[ObservableProperty] private string _name = "";
[ObservableProperty] private string _iconKey = "Folder";
/// <summary>Hex colour for name + guide line; null = theme default.</summary>
[ObservableProperty] private string? _color;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowCount))]
private bool _isCollapsed;
@@ -352,6 +352,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
Id = f.Id,
Name = f.Name,
IconKey = f.Icon ?? ListFolderIcon,
Color = f.Color,
IsCollapsed = f.IsCollapsed,
ParentFolderId = f.ParentFolderId,
};
@@ -758,7 +759,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
if (folder is null || Dialogs is null || _services is null) return;
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
vm.BeginLoadFolder(folder.Id, folder.Name, folder.IconKey);
vm.BeginLoadFolder(folder.Id, folder.Name, folder.IconKey, folder.Color);
await Dialogs.ShowListSettingsAsync(vm);
// Rename, re-icon and delete all change the folder header (and delete releases its lists
// back to the top level) — one reload covers every outcome.
@@ -115,6 +115,13 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
public const string DefaultIcon = "Dot";
/// <summary>Folder colour swatches; "" = no colour (theme default).</summary>
public static readonly string[] FolderColorOptions =
["", "#D4A574", "#C87060", "#D9C27A", "#8B9D7A", "#6FB3A8", "#7FA7C9", "#A38BC9"];
/// <summary>Selected folder colour, "" for none.</summary>
[ObservableProperty] private string _folderColor = "";
/// <summary>Default icon for folders, kept apart from <see cref="DefaultIcon"/> so lists never look like folders.</summary>
public const string DefaultFolderIcon = "Folder";
@@ -229,8 +236,9 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
/// Opens the modal on an existing folder: name + icon only, no list config to load and
/// nothing to fetch from the worker, so this needs no async counterpart.
/// </summary>
public void BeginLoadFolder(string folderId, string name, string? icon)
public void BeginLoadFolder(string folderId, string name, string? icon, string? color = null)
{
FolderColor = color ?? "";
_folderId = folderId;
IsFolder = true;
CanToggleFolder = false;
@@ -375,6 +383,8 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
CloseAction?.Invoke();
}
private static string? NullIfEmpty(string s) => s.Length == 0 ? null : s;
private async Task SaveFolderAsync()
{
var name = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name;
@@ -388,6 +398,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
Id = Guid.NewGuid().ToString("N"),
Name = name,
Icon = Icon,
Color = NullIfEmpty(FolderColor),
CreatedAt = DateTime.UtcNow,
});
// The create flow pre-creates a list row for this modal to save against; a folder
@@ -398,7 +409,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase
}
else
{
await lists.RenameFolderAsync(_folderId, name, Icon);
await lists.RenameFolderAsync(_folderId, name, Icon, NullIfEmpty(FolderColor));
}
Saved = true;