Expanding theme tab to customization tab. Started improving theme selection.

This commit is contained in:
2025-07-20 23:27:51 +02:00
parent 03ea94b858
commit 2c9a87bf3e
20 changed files with 1336 additions and 295 deletions

View File

@@ -0,0 +1,45 @@
<label for="@Id" class="btn btn-square border-0 ring-0 outline-0" style="background-color: @Value">
<i class="text-lg text-base-content @Icon"></i>
</label>
<input value="@Value" @oninput="Update" id="@Id" type="color" class="h-0 w-0 opacity-0"/>
@code
{
#region
[Parameter]
public string? Value
{
get => _value;
set
{
if (_value?.Equals(value) ?? false)
return;
_value = value;
ValueChanged.InvokeAsync(value);
}
}
[Parameter] public EventCallback<string?> ValueChanged { get; set; }
private string? _value;
#endregion
[Parameter] public string Icon { get; set; } = "icon-paintbrush";
private string Id;
protected override void OnInitialized()
{
Id = $"color-selector-{GetHashCode()}";
}
private async Task Update(ChangeEventArgs args)
{
Value = args.Value?.ToString() ?? "#FFFFFF";
await InvokeAsync(StateHasChanged);
}
}

View File

@@ -13,7 +13,7 @@
<span class="@Icon text-4xl text-primary"></span>
</div>
<div class="stat-title">@Title</div>
<div class="stat-value text-xl!">@Text</div>
<div class="stat-value truncate text-xl!">@Text</div>
</div>
@code

View File

@@ -1,259 +0,0 @@
@using System.Text.Json
@using MoonCore.Helpers
@using Moonlight.Client.Services
@using Moonlight.Client.UI.Components
@using Moonlight.Shared.Http.Requests.Admin.Sys
@using Moonlight.Shared.Misc
@inject HttpApiClient ApiClient
@inject FrontendConfiguration FrontendConfiguration
@inject ThemeService ThemeService
@inject ToastService ToastService
@* @inject DownloadService DownloadService *@
<div class="card card-body p-2">
<div class="flex flex-row items-center justify-end gap-x-2">
<WButton OnClick="_ => Save()" CssClasses="btn btn-success">
<i class="icon-save me-1"></i>
<span>Save</span>
</WButton>
<InputFile OnChange="Import" id="import-file" hidden="hidden" />
<label for="import-file" class="btn btn-info cursor-pointer">
<i class="icon-file-up me-1"></i>
<span>Import</span>
</label>
<WButton OnClick="_ => Export()" CssClasses="btn btn-warning">
<i class="icon-download me-1"></i>
<span>Export</span>
</WButton>
</div>
</div>
<div class="mt-5 grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-5">
@foreach (var colorSettingGroup in GroupedColorSettings)
{
<div class="flex flex-col gap-y-2">
@foreach (var colorSetting in colorSettingGroup.Value)
{
<ThemeColorSelector Identifier="@colorSetting.Identifier"
Value="@colorSetting.Value"
DefaultValue="@colorSetting.DefaultValue"
OnChanged="color => OnChanged(colorSetting, color)"/>
}
</div>
}
</div>
@code
{
private readonly Dictionary<string, List<ColorSetting>> GroupedColorSettings = new();
protected override void OnInitialized()
{
// Primary
AddSetting("primary", "primary-50", 238, 242, 255);
AddSetting("primary", "primary-100", 224, 231, 255);
AddSetting("primary", "primary-200", 199, 210, 254);
AddSetting("primary", "primary-300", 165, 180, 252);
AddSetting("primary", "primary-400", 129, 140, 248);
AddSetting("primary", "primary", 99, 102, 241);
AddSetting("primary", "primary-600", 79, 70, 229);
AddSetting("primary", "primary-700", 67, 56, 202);
AddSetting("primary", "primary-800", 55, 48, 163);
AddSetting("primary", "primary-900", 49, 46, 129);
AddSetting("primary", "primary-950", 30, 27, 75);
// Secondary
AddSetting("secondary", "secondary-100", 249, 249, 249);
AddSetting("secondary", "secondary-200", 241, 241, 242);
AddSetting("secondary", "secondary-300", 219, 223, 233);
AddSetting("secondary", "secondary-400", 181, 181, 195);
AddSetting("secondary", "secondary-500", 153, 161, 183);
AddSetting("secondary", "secondary-600", 112, 121, 147);
AddSetting("secondary", "secondary-700", 68, 78, 107);
AddSetting("secondary", "secondary-800", 28, 36, 56);
AddSetting("secondary", "secondary-900", 17, 23, 33);
AddSetting("secondary", "secondary-950", 14, 18, 28);
// Tertiary
AddSetting("tertiary", "tertiary-50", 245, 243, 255);
AddSetting("tertiary", "tertiary-100", 237, 233, 254);
AddSetting("tertiary", "tertiary-200", 221, 214, 254);
AddSetting("tertiary", "tertiary-300", 196, 181, 253);
AddSetting("tertiary", "tertiary-400", 167, 139, 250);
AddSetting("tertiary", "tertiary", 139, 92, 246);
AddSetting("tertiary", "tertiary-600", 124, 58, 237);
AddSetting("tertiary", "tertiary-700", 109, 40, 217);
AddSetting("tertiary", "tertiary-800", 91, 33, 182);
AddSetting("tertiary", "tertiary-900", 76, 29, 149);
AddSetting("tertiary", "tertiary-950", 46, 16, 101);
// Warning
AddSetting("warning", "warning-50", 254, 252, 232);
AddSetting("warning", "warning-100", 254, 249, 195);
AddSetting("warning", "warning-200", 254, 240, 138);
AddSetting("warning", "warning-300", 253, 224, 71);
AddSetting("warning", "warning-400", 250, 204, 21);
AddSetting("warning", "warning-500", 234, 179, 8);
AddSetting("warning", "warning-600", 202, 138, 4);
AddSetting("warning", "warning-700", 161, 98, 7);
AddSetting("warning", "warning-800", 133, 77, 14);
AddSetting("warning", "warning-900", 113, 63, 18);
AddSetting("warning", "warning-950", 66, 32, 6);
// Danger
AddSetting("danger", "danger-50", 254, 242, 242);
AddSetting("danger", "danger-100", 254, 226, 226);
AddSetting("danger", "danger-200", 254, 202, 202);
AddSetting("danger", "danger-300", 252, 165, 165);
AddSetting("danger", "danger-400", 248, 113, 113);
AddSetting("danger", "danger", 239, 68, 68);
AddSetting("danger", "error", 220, 38, 38);
AddSetting("danger", "danger-700", 185, 28, 28);
AddSetting("danger", "danger-800", 153, 27, 27);
AddSetting("danger", "danger-900", 127, 29, 29);
AddSetting("danger", "danger-950", 69, 10, 10);
// Success
AddSetting("success", "success-50", 240, 253, 244);
AddSetting("success", "success-100", 220, 252, 231);
AddSetting("success", "success-200", 187, 247, 208);
AddSetting("success", "success-300", 134, 239, 172);
AddSetting("success", "success-400", 74, 222, 128);
AddSetting("success", "success", 34, 197, 94);
AddSetting("success", "success-600", 22, 163, 74);
AddSetting("success", "success-700", 21, 128, 61);
AddSetting("success", "success-800", 22, 101, 52);
AddSetting("success", "success-900", 20, 83, 45);
AddSetting("success", "success-950", 5, 46, 22);
// Info
AddSetting("info", "info-50", 239, 246, 255);
AddSetting("info", "info-100", 219, 234, 254);
AddSetting("info", "info-200", 191, 219, 254);
AddSetting("info", "info-300", 147, 197, 253);
AddSetting("info", "info-400", 96, 165, 250);
AddSetting("info", "info-500", 59, 130, 246);
AddSetting("info", "info-600", 37, 99, 235);
AddSetting("info", "info-700", 29, 78, 216);
AddSetting("info", "info-800", 30, 64, 175);
AddSetting("info", "info-900", 30, 58, 138);
AddSetting("info", "info-950", 23, 37, 84);
// Gray
AddSetting("gray", "gray-100", 249, 249, 249);
AddSetting("gray", "gray-200", 241, 241, 242);
AddSetting("gray", "gray-300", 219, 223, 233);
AddSetting("gray", "gray-400", 181, 181, 195);
AddSetting("gray", "gray-500", 153, 161, 183);
AddSetting("gray", "gray-600", 112, 121, 147);
AddSetting("gray", "gray-700", 68, 78, 107);
AddSetting("gray", "gray-750", 41, 50, 73);
AddSetting("gray", "gray-800", 28, 36, 56);
AddSetting("gray", "gray-900", 17, 23, 33);
AddSetting("gray", "gray-950", 14, 18, 28);
// Full
AddSetting("full", "light", 255, 255, 255);
AddSetting("full", "dark", 0, 0, 0);
}
private void AddSetting(string group, string identifier, int r, int g, int b)
{
if (!GroupedColorSettings.ContainsKey(group))
GroupedColorSettings[group] = new();
var value = ThemeService.Variables.GetValueOrDefault(identifier);
GroupedColorSettings[group].Add(new ColorSetting()
{
Identifier = identifier,
DefaultValue = $"{r} {g} {b}",
Value = value
});
}
private async Task OnChanged(ColorSetting colorSetting, string color)
{
if (color == colorSetting.DefaultValue)
colorSetting.Value = null;
else
colorSetting.Value = color;
if (colorSetting.Value == null && ThemeService.Variables.ContainsKey(colorSetting.Identifier))
ThemeService.Variables.Remove(colorSetting.Identifier);
else if (colorSetting.Value != null)
ThemeService.Variables[colorSetting.Identifier] = colorSetting.Value;
await ThemeService.Refresh();
}
private async Task Save()
{
if (FrontendConfiguration.HostEnvironment != "ApiServer")
{
await ToastService.Error(
"Theme Settings",
"Unable to save the theme settings. If you are using a static host, you need to configure the colors in the frontend.json file"
);
return;
}
// Send new variables
await ApiClient.Patch("api/admin/system/theme", new UpdateThemeRequest()
{
Variables = ThemeService.Variables
});
await ToastService.Success("Successfully saved theme settings");
}
private async Task Export()
{
// Serialize the variables
var json = JsonSerializer.Serialize(ThemeService.Variables);
// Download the theme configuration
//await DownloadService.DownloadString("theme.json", json);
await ToastService.Success("Successfully exported theme configuration");
}
private async Task Import(InputFileChangeEventArgs eventArgs)
{
if (!eventArgs.File.Name.EndsWith(".json"))
{
await ToastService.Error("Only .json files are allowed");
return;
}
// Read file content
var stream = eventArgs.File.OpenReadStream();
var sr = new StreamReader(stream);
var json = await sr.ReadToEndAsync();
// Deserialize
var variables = JsonSerializer.Deserialize<Dictionary<string, string>>(json) ?? new();
// Update variables
ThemeService.Variables.Clear();
foreach (var variable in variables)
ThemeService.Variables[variable.Key] = variable.Value;
// Apply changes
await ThemeService.Refresh();
//
await ToastService.Success("Successfully imported theme configuration");
}
class ColorSetting
{
public string Identifier { get; set; }
public string DefaultValue { get; set; }
public string? Value { get; set; }
}
}

View File

@@ -1,23 +1,197 @@
@page "/admin/system/theme"
@using Microsoft.AspNetCore.Authorization
@using Moonlight.Client.UI.Partials.Design
@using Moonlight.Client.UI.Components
@using Moonlight.Shared.Misc
@attribute [Authorize(Policy = "permissions:admin.system.theme")]
<div class="mb-5">
<NavTabs Index="1" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks" />
</div>
<NavTabs Index="1" Names="UiConstants.AdminNavNames" Links="UiConstants.AdminNavLinks"/>
<div class="mt-5">
<div class="mockup-browser bg-base-300/40">
<div class="mockup-browser-toolbar">
<div class="input bg-base-200">https://your-moonlight.instance</div>
<div class="mt-5 grid grid-cols-3 gap-3">
<div class="col-span-1 flex flex-col gap-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBackground"/>
<span class="ms-1">Background</span>
</div>
<div class="flex h-80 justify-center">
<iframe src="http://localhost:5165/admin/system" class="w-full object-cover" >
</iframe>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBaseContent"/>
<span class="ms-1">Base Content</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBase100"/>
<span class="ms-1">Base 100</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBase150"/>
<span class="ms-1">Base 150</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBase200"/>
<span class="ms-1">Base 200</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBase250"/>
<span class="ms-1">Base 250</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorBase300"/>
<span class="ms-1">Base 300</span>
</div>
</div>
<div class="col-span-2 grid grid-cols-2 gap-3">
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorPrimary"/>
<span class="ms-1">Primary</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorPrimaryContent"/>
<span class="ms-1">Primary Content</span>
</div>
</div>
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorSecondary"/>
<span class="ms-1">Secondary</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorSecondaryContent"/>
<span class="ms-1">Secondary Content</span>
</div>
</div>
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorAccent"/>
<span class="ms-1">Accent</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorAccentContent"/>
<span class="ms-1">Accent Content</span>
</div>
</div>
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorInfo"/>
<span class="ms-1">Info</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorInfoContent"/>
<span class="ms-1">Info Content</span>
</div>
</div>
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorSuccess"/>
<span class="ms-1">Success</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorSuccessContent"/>
<span class="ms-1">Success Content</span>
</div>
</div>
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorWarning"/>
<span class="ms-1">Warning</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorWarningContent"/>
<span class="ms-1">Warning Content</span>
</div>
</div>
<div class="col-span-1 flex flex-col gap-y-3">
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector @bind-Value="ThemeData.ColorError"/>
<span class="ms-1">Error</span>
</div>
<div class="card card-body flex-row grow-0 items-center p-1.5 text-base-content">
<ColorSelector Icon="icon-type" @bind-Value="ThemeData.ColorErrorContent"/>
<span class="ms-1">Error Content</span>
</div>
</div>
</div>
</div>
@code
{
private ApplicationTheme ThemeData;
protected override void OnInitialized()
{
ThemeData = CreateDefault();
}
private ApplicationTheme CreateDefault()
{
return new ApplicationTheme()
{
ColorBackground = "#0c0f18",
ColorBase100 = "#1e2b47",
ColorBase150 = "#1a2640",
ColorBase200 = "#101a2e",
ColorBase250 = "#0f1729",
ColorBase300 = "#0c1221",
ColorBaseContent = "#dde5f5",
ColorPrimary = "#4f39f6",
ColorPrimaryContent = "#dde5f5",
ColorSecondary = "#354052",
ColorSecondaryContent = "#dde5f5",
ColorAccent = "#ad46ff",
ColorAccentContent = "#dde5f5",
ColorNeutral = "#dde5f5",
ColorNeutralContent = "#09090b",
ColorInfo = "#155dfc",
ColorInfoContent = "#dde5f5",
ColorSuccess = "#00a63e",
ColorSuccessContent = "#dde5f5",
ColorWarning = "#ffba00",
ColorWarningContent = "#dde5f5",
ColorError = "#ec003f",
ColorErrorContent = "#dde5f5",
RadiusSelector = 0.25f,
RadiusField = 0.5f,
RadiusBox = 0.5f,
SizeSelector = 0.25f,
SizeField = 0.25f,
Border = 1f,
Depth = 0f,
Noise = 0f
};
}
}