@@ -55,6 +59,12 @@
}
+ @if (ThemesAccess.Succeeded)
+ {
+
+
+
+ }
@code
@@ -66,12 +76,14 @@
[CascadingParameter] public Task
AuthState { get; set; }
private AuthorizationResult ApiKeyAccess;
+ private AuthorizationResult ThemesAccess;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
ApiKeyAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.ApiKeys.View);
+ ThemesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.View);
}
private void OnTabChanged(string name)
diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor
new file mode 100644
index 00000000..66b3a14a
--- /dev/null
+++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Create.razor
@@ -0,0 +1,134 @@
+@page "/admin/system/themes/create"
+
+@using Microsoft.AspNetCore.Authorization
+@using Moonlight.Shared
+@using LucideBlazor
+@using Moonlight.Shared.Http.Requests.Themes
+@using ShadcnBlazor.Buttons
+@using ShadcnBlazor.Labels
+@using ShadcnBlazor.Cards
+@using ShadcnBlazor.Checkboxes
+@using ShadcnBlazor.Extras.Editors
+@using ShadcnBlazor.Extras.FormHandlers
+@using ShadcnBlazor.Extras.Toasts
+@using ShadcnBlazor.Inputs
+
+@attribute [Authorize(Policy = Permissions.Themes.Create)]
+
+@inject HttpClient HttpClient
+@inject NavigationManager Navigation
+@inject ToastService ToastService
+
+
+
+
Create theme
+
+ Create a new theme
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code
+{
+ private CreateThemeDto Request = new()
+ {
+ CssContent = "/* Define your css here */"
+ };
+
+ private FormHandler Form;
+ private Editor Editor;
+
+ private async Task SubmitAsync()
+ {
+ Request.CssContent = await Editor.GetValueAsync();
+ await Form.SubmitAsync();
+ }
+
+ private async Task OnSubmitAsync()
+ {
+ await HttpClient.PostAsJsonAsync(
+ "/api/admin/themes",
+ Request,
+ Constants.SerializerOptions
+ );
+
+ await ToastService.SuccessAsync(
+ "Theme creation",
+ $"Successfully created theme {Request.Name}"
+ );
+
+ Navigation.NavigateTo("/admin/system?tab=themes");
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Index.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Index.razor
new file mode 100644
index 00000000..17208173
--- /dev/null
+++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Index.razor
@@ -0,0 +1,148 @@
+@using LucideBlazor
+@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Components.Authorization
+@using Moonlight.Shared
+@using Moonlight.Shared.Http.Requests
+@using Moonlight.Shared.Http.Responses
+@using Moonlight.Shared.Http.Responses.Themes
+@using ShadcnBlazor.DataGrids
+@using ShadcnBlazor.Dropdowns
+@using ShadcnBlazor.Extras.AlertDialogs
+@using ShadcnBlazor.Tabels
+@using ShadcnBlazor.Buttons
+@using ShadcnBlazor.Extras.Toasts
+
+@inject ToastService ToastService
+@inject NavigationManager Navigation
+@inject AlertDialogService AlertDialogService
+@inject IAuthorizationService AuthorizationService
+@inject HttpClient HttpClient
+
+
+
+
Themes
+
+ Manage themes for your instance
+
+
+
+
+
+
+
+@code
+{
+ [CascadingParameter] public Task AuthState { get; set; }
+
+ private DataGrid Grid;
+
+ private AuthorizationResult EditAccess;
+ private AuthorizationResult DeleteAccess;
+ private AuthorizationResult CreateAccess;
+
+ protected override async Task OnInitializedAsync()
+ {
+ var authState = await AuthState;
+
+ EditAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.Edit);
+ DeleteAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.Delete);
+ CreateAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.Create);
+ }
+
+ private async Task> LoadAsync(DataGridRequest request)
+ {
+ var query = $"?startIndex={request.StartIndex}&length={request.Length}";
+ var filterOptions = request.Filters.Count > 0 ? new FilterOptions(request.Filters) : null;
+
+ var response = await HttpClient.GetFromJsonAsync>(
+ $"api/admin/themes{query}&filterOptions={filterOptions}",
+ Constants.SerializerOptions
+ );
+
+ return new DataGridResponse(response!.Data, response.TotalLength);
+ }
+
+ private void CreateAsync() => Navigation.NavigateTo("/admin/system/themes/create");
+
+ private void EditAsync(ThemeDto theme) => Navigation.NavigateTo($"/admin/system/themes/{theme.Id}");
+
+ private async Task DeleteAsync(ThemeDto theme)
+ {
+ await AlertDialogService.ConfirmDangerAsync(
+ $"Deletion of theme {theme.Name}",
+ "Do you really want to delete this theme? This action cannot be undone",
+ async () =>
+ {
+ var response = await HttpClient.DeleteAsync($"api/admin/themes/{theme.Id}");
+ response.EnsureSuccessStatusCode();
+
+ await ToastService.SuccessAsync("Theme deletion", $"Successfully deleted theme {theme.Name}");
+
+ await Grid.RefreshAsync();
+ }
+ );
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor
new file mode 100644
index 00000000..df0b4a33
--- /dev/null
+++ b/Moonlight.Frontend/UI/Admin/Views/Sys/Themes/Update.razor
@@ -0,0 +1,147 @@
+@page "/admin/system/themes/{Id:int}"
+
+@using Microsoft.AspNetCore.Authorization
+@using Moonlight.Shared
+@using LucideBlazor
+@using Moonlight.Frontend.Mappers
+@using Moonlight.Shared.Http.Requests.Themes
+@using Moonlight.Shared.Http.Responses.Themes
+@using ShadcnBlazor.Buttons
+@using ShadcnBlazor.Labels
+@using ShadcnBlazor.Cards
+@using ShadcnBlazor.Checkboxes
+@using ShadcnBlazor.Extras.Common
+@using ShadcnBlazor.Extras.Editors
+@using ShadcnBlazor.Extras.FormHandlers
+@using ShadcnBlazor.Extras.Toasts
+@using ShadcnBlazor.Inputs
+
+@attribute [Authorize(Policy = Permissions.Themes.Edit)]
+
+@inject HttpClient HttpClient
+@inject NavigationManager Navigation
+@inject ToastService ToastService
+
+
+
+
Update theme
+
+ Update the theme
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+@code
+{
+ [Parameter] public int Id { get; set; }
+
+ private UpdateThemeDto Request;
+ private ThemeDto Theme;
+
+ private FormHandler Form;
+ private Editor Editor;
+
+ private async Task LoadAsync(LazyLoader _)
+ {
+ var theme = await HttpClient.GetFromJsonAsync($"api/admin/themes/{Id}");
+
+ Theme = theme!;
+ Request = ThemeMapper.ToUpdate(Theme);
+ }
+
+ private async Task SubmitAsync()
+ {
+ Request.CssContent = await Editor.GetValueAsync();
+ await Form.SubmitAsync();
+ }
+
+ private async Task OnSubmitAsync()
+ {
+ await HttpClient.PatchAsJsonAsync(
+ $"/api/admin/themes/{Theme.Id}",
+ Request,
+ Constants.SerializerOptions
+ );
+
+ await ToastService.SuccessAsync(
+ "Theme update",
+ $"Successfully updated theme {Request.Name}"
+ );
+
+ Navigation.NavigateTo("/admin/system?tab=themes");
+ }
+}
\ No newline at end of file
diff --git a/Moonlight.Shared/Http/Requests/Themes/CreateThemeDto.cs b/Moonlight.Shared/Http/Requests/Themes/CreateThemeDto.cs
new file mode 100644
index 00000000..01ebf5b0
--- /dev/null
+++ b/Moonlight.Shared/Http/Requests/Themes/CreateThemeDto.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Moonlight.Shared.Http.Requests.Themes;
+
+public class CreateThemeDto
+{
+ [Required]
+ [MaxLength(30)]
+ public string Name { get; set; }
+
+ [Required]
+ [MaxLength(30)]
+ public string Version { get; set; }
+
+ [Required]
+ [MaxLength(30)]
+ public string Author { get; set; }
+ public bool IsEnabled { get; set; }
+
+ [Required]
+ [MaxLength(20_000)]
+ public string CssContent { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight.Shared/Http/Requests/Themes/UpdateThemeDto.cs b/Moonlight.Shared/Http/Requests/Themes/UpdateThemeDto.cs
new file mode 100644
index 00000000..0d22d223
--- /dev/null
+++ b/Moonlight.Shared/Http/Requests/Themes/UpdateThemeDto.cs
@@ -0,0 +1,23 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Moonlight.Shared.Http.Requests.Themes;
+
+public class UpdateThemeDto
+{
+ [Required]
+ [MaxLength(30)]
+ public string Name { get; set; }
+
+ [Required]
+ [MaxLength(30)]
+ public string Version { get; set; }
+
+ [Required]
+ [MaxLength(30)]
+ public string Author { get; set; }
+ public bool IsEnabled { get; set; }
+
+ [Required]
+ [MaxLength(20_000)]
+ public string CssContent { get; set; }
+}
\ No newline at end of file
diff --git a/Moonlight.Shared/Http/Responses/Frontend/FrontendConfigDto.cs b/Moonlight.Shared/Http/Responses/Frontend/FrontendConfigDto.cs
new file mode 100644
index 00000000..747e5d39
--- /dev/null
+++ b/Moonlight.Shared/Http/Responses/Frontend/FrontendConfigDto.cs
@@ -0,0 +1,3 @@
+namespace Moonlight.Shared.Http.Responses.Frontend;
+
+public record FrontendConfigDto(string Name, string? ThemeCss);
\ No newline at end of file
diff --git a/Moonlight.Shared/Http/Responses/Themes/ThemeDto.cs b/Moonlight.Shared/Http/Responses/Themes/ThemeDto.cs
new file mode 100644
index 00000000..adc167dd
--- /dev/null
+++ b/Moonlight.Shared/Http/Responses/Themes/ThemeDto.cs
@@ -0,0 +1,3 @@
+namespace Moonlight.Shared.Http.Responses.Themes;
+
+public record ThemeDto(int Id, string Name, string Author, string Version, string CssContent, bool IsEnabled);
\ No newline at end of file
diff --git a/Moonlight.Shared/Http/SerializationContext.cs b/Moonlight.Shared/Http/SerializationContext.cs
index 2de40ed1..e3739a64 100644
--- a/Moonlight.Shared/Http/SerializationContext.cs
+++ b/Moonlight.Shared/Http/SerializationContext.cs
@@ -1,32 +1,48 @@
using System.Text.Json.Serialization;
using Moonlight.Shared.Http.Requests.ApiKeys;
using Moonlight.Shared.Http.Requests.Roles;
+using Moonlight.Shared.Http.Requests.Themes;
using Moonlight.Shared.Http.Requests.Users;
using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Shared.Http.Responses.ApiKeys;
using Moonlight.Shared.Http.Responses.Auth;
+using Moonlight.Shared.Http.Responses.Themes;
using Moonlight.Shared.Http.Responses.Users;
namespace Moonlight.Shared.Http;
+// Users
[JsonSerializable(typeof(CreateUserDto))]
[JsonSerializable(typeof(UpdateUserDto))]
+[JsonSerializable(typeof(PagedData))]
+[JsonSerializable(typeof(UserDto))]
+
+// Auth
[JsonSerializable(typeof(ClaimDto[]))]
[JsonSerializable(typeof(SchemeDto[]))]
+
+// System
[JsonSerializable(typeof(DiagnoseResultDto[]))]
-[JsonSerializable(typeof(UserDto))]
[JsonSerializable(typeof(SystemInfoDto))]
-[JsonSerializable(typeof(PagedData))]
-[JsonSerializable(typeof(PagedData))]
-[JsonSerializable(typeof(RoleDto))]
+
+// Roles
[JsonSerializable(typeof(CreateRoleDto))]
[JsonSerializable(typeof(UpdateRoleDto))]
+[JsonSerializable(typeof(PagedData))]
+[JsonSerializable(typeof(RoleDto))]
+
+// API Keys
[JsonSerializable(typeof(CreateApiKeyDto))]
[JsonSerializable(typeof(UpdateApiKeyDto))]
-[JsonSerializable(typeof(UpdateApiKeyDto))]
[JsonSerializable(typeof(PagedData))]
[JsonSerializable(typeof(ApiKeyDto))]
+
+// Themes
+[JsonSerializable(typeof(CreateThemeDto))]
+[JsonSerializable(typeof(UpdateThemeDto))]
+[JsonSerializable(typeof(PagedData))]
+[JsonSerializable(typeof(ThemeDto))]
public partial class SerializationContext : JsonSerializerContext
{
}
\ No newline at end of file
diff --git a/Moonlight.Shared/Permissions.cs b/Moonlight.Shared/Permissions.cs
index 2d96d25d..aa565ca2 100644
--- a/Moonlight.Shared/Permissions.cs
+++ b/Moonlight.Shared/Permissions.cs
@@ -37,6 +37,16 @@ public static class Permissions
public const string Members = $"{Prefix}{Section}.{nameof(Members)}";
}
+ public static class Themes
+ {
+ private const string Section = "Themes";
+
+ public const string View = $"{Prefix}{Section}.{nameof(View)}";
+ public const string Edit = $"{Prefix}{Section}.{nameof(Edit)}";
+ public const string Create = $"{Prefix}{Section}.{nameof(Create)}";
+ public const string Delete = $"{Prefix}{Section}.{nameof(Delete)}";
+ }
+
public static class System
{
private const string Section = "System";