15 Commits

Author SHA1 Message Date
3dff8c8f6d Fixed IL trimming removing used icons from build output in system settings tab 2026-02-21 22:46:34 +01:00
95a848e571 Merge pull request 'Implemented extendable system settings tab. Started implementing white labeling settings' (#21) from feat/SystemSettings into v2.1
Reviewed-on: #21
2026-02-21 21:22:06 +00:00
9d557eea4e Implemented extendable system settings tab. Started implementing white labeling settings 2026-02-21 22:20:51 +01:00
94c1aac0ac Merge pull request 'Added plugins hooks for layout related options' (#20) from feat/LayoutMiddleware into v2.1
Reviewed-on: #20
2026-02-20 15:28:24 +00:00
3bddd64d91 Added page hooks for main layout 2026-02-20 16:25:01 +01:00
5ad7a6db7b Added hook option for plugins to inject into the main layout before the router 2026-02-20 12:28:22 +01:00
9b9272cd6e Fixed nuget package build failing after changing shadcnblazor class list location 2026-02-20 09:41:35 +01:00
31cf34ed04 Merge pull request 'Improved css build and initialization of frontend plugins' (#19) from feat/PluginImprovements into v2.1
Some checks failed
Dev Publish: Nuget / Publish Dev Packages (push) Failing after 40s
Reviewed-on: #19
2026-02-20 08:38:27 +00:00
a9b0020131 Upgraded to shadcnblazor 1.0.13. Added transitive mapping copying and prefixed target to stop any collisions 2026-02-20 09:35:43 +01:00
e3b432aae6 Removed unused startup interface. Added plugin list to frontend plugin initialization 2026-02-20 09:20:29 +01:00
06f27605ba Merge pull request 'Switched from self created static constant json options to a source generator options' (#18) from feat/ImproveJsonSerialization into v2.1
Reviewed-on: #18
2026-02-19 07:50:03 +00:00
0bd138df63 Switched from self created static constant json options to a source generator options 2026-02-19 08:49:23 +01:00
d7b725f541 Merge pull request 'Improved plugin loading and handling' (#17) from feat/ImprovePluginLoading into v2.1
All checks were successful
Dev Publish: Nuget / Publish Dev Packages (push) Successful in 51s
Reviewed-on: #17
2026-02-19 07:36:06 +00:00
0f26aaf803 Added options for navigation assemblies for the router 2026-02-19 08:32:32 +01:00
c45e177001 Improved handling of moonlight plugins during startup, minimized host project code and moved startup handling to core 2026-02-18 15:36:45 +01:00
55 changed files with 663 additions and 209 deletions

View File

@@ -34,7 +34,7 @@ jobs:
# Publish frontend # Publish frontend
# We need to build it first so the class list files generate # We need to build it first so the class list files generate
- name: Build project - name: Build project
run: dotnet build Moonlight.Frontend --configuration Debug run: dotnet build Hosts/Moonlight.Frontend.Host --configuration Debug
- name: Build tailwind styles and extract class list - name: Build tailwind styles and extract class list
working-directory: Hosts/Moonlight.Frontend.Host/Styles working-directory: Hosts/Moonlight.Frontend.Host/Styles

View File

@@ -7,9 +7,6 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,27 +1,9 @@
using Moonlight.Api.Startup; using Moonlight.Api;
using SimplePlugin.Generated; using SimplePlugin.Generated;
var modules = PluginRegistry.Modules var plugins = PluginRegistry
.OfType<IAppStartup>() .Modules
.OfType<MoonlightPlugin>()
.ToArray(); .ToArray();
var builder = WebApplication.CreateBuilder(args); await StartupHandler.RunAsync(args, plugins);
foreach (var startup in modules)
startup.PreBuild(builder);
var app = builder.Build();
foreach (var startup in modules)
startup.PostBuild(app);
foreach (var startup in modules)
startup.PostMiddleware(app);
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
await app.RunAsync();

View File

@@ -1,20 +1,9 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Moonlight.Frontend;
using Moonlight.Frontend.Startup;
using SimplePlugin.Generated; using SimplePlugin.Generated;
var modules = PluginRegistry var plugins = PluginRegistry
.Modules .Modules
.OfType<IAppStartup>() .OfType<MoonlightPlugin>()
.ToArray(); .ToArray();
var builder = WebAssemblyHostBuilder.CreateDefault(args); await StartupHandler.RunAsync(args, plugins);
foreach (var startup in modules)
startup.PreBuild(builder);
var app = builder.Build();
foreach(var startup in modules)
startup.PostBuild(app);
await app.RunAsync();

View File

@@ -1,11 +1,11 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/scrollbar.css"; @import "../bin/ShadcnBlazor/scrollbar.css";
@import "../../../Moonlight.Frontend/bin/ShadcnBlazor/default-theme.css"; @import "../bin/ShadcnBlazor/default-theme.css";
@import "./theme.css"; @import "./theme.css";
@source "../../../Moonlight.Frontend/bin/ShadcnBlazor/ShadcnBlazor.map"; @source "../bin/ShadcnBlazor/ShadcnBlazor.map";
@source "../../../Moonlight.Api/**/*.razor"; @source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs"; @source "../../../Moonlight.Api/**/*.cs";

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Api.Constants;
public class FrontendSettingConstants
{
public const string Name = "Moonlight.Frontend.Name";
}

View File

@@ -0,0 +1,49 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Moonlight.Api.Constants;
using Moonlight.Api.Services;
using Moonlight.Shared;
using Moonlight.Shared.Http.Requests.Admin.Settings;
using Moonlight.Shared.Http.Responses.Admin.Settings;
namespace Moonlight.Api.Http.Controllers.Admin.Settings;
[ApiController]
[Authorize(Policy = Permissions.System.Settings)]
[Route("api/admin/system/settings/whiteLabeling")]
public class WhiteLabelingController : Controller
{
private readonly SettingsService SettingsService;
private readonly FrontendService FrontendService;
public WhiteLabelingController(SettingsService settingsService, FrontendService frontendService)
{
SettingsService = settingsService;
FrontendService = frontendService;
}
[HttpGet]
public async Task<ActionResult<WhiteLabelingDto>> GetAsync()
{
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
};
return dto;
}
[HttpPost]
public async Task<ActionResult<WhiteLabelingDto>> PostAsync([FromBody] SetWhiteLabelingDto request)
{
await SettingsService.SetValueAsync(FrontendSettingConstants.Name, request.Name);
await FrontendService.ResetCacheAsync();
var dto = new WhiteLabelingDto
{
Name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name) ?? "Moonlight"
};
return dto;
}
}

View File

@@ -9,21 +9,9 @@ namespace Moonlight.Api.Http.Services.ContainerHelper;
[JsonSerializable(typeof(ProblemDetails))] [JsonSerializable(typeof(ProblemDetails))]
[JsonSerializable(typeof(RebuildEventDto))] [JsonSerializable(typeof(RebuildEventDto))]
[JsonSerializable(typeof(RequestRebuildDto))] [JsonSerializable(typeof(RequestRebuildDto))]
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
public partial class SerializationContext : JsonSerializerContext public partial class SerializationContext : JsonSerializerContext
{ {
private static JsonSerializerOptions? InternalTunedOptions;
public static JsonSerializerOptions TunedOptions
{
get
{
if (InternalTunedOptions != null)
return InternalTunedOptions;
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
return InternalTunedOptions;
}
}
} }

View File

@@ -25,6 +25,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.1"/>
<PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="10.3.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" /> <PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.3" />

View File

@@ -0,0 +1,28 @@
using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using SimplePlugin.Abstractions;
namespace Moonlight.Api;
public abstract class MoonlightPlugin : IPluginModule
{
protected MoonlightPlugin[] Plugins { get; private set; }
public void Initialize(MoonlightPlugin[] plugins)
{
Plugins = plugins;
}
public virtual void PreBuild(WebApplicationBuilder builder)
{
}
public virtual void PostBuild(WebApplication application)
{
}
public virtual void PostMiddleware(WebApplication application)
{
}
}

View File

@@ -1,5 +1,4 @@
using System.Net.Http.Headers; using System.Net.Http.Json;
using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using Moonlight.Api.Http.Services.ContainerHelper; using Moonlight.Api.Http.Services.ContainerHelper;
using Moonlight.Api.Http.Services.ContainerHelper.Requests; using Moonlight.Api.Http.Services.ContainerHelper.Requests;
@@ -42,7 +41,7 @@ public class ContainerHelperService
request.Content = JsonContent.Create( request.Content = JsonContent.Create(
new RequestRebuildDto(noBuildCache), new RequestRebuildDto(noBuildCache),
null, null,
SerializationContext.TunedOptions SerializationContext.Default.Options
); );
var response = await client.SendAsync( var response = await client.SendAsync(
@@ -77,7 +76,7 @@ public class ContainerHelperService
continue; continue;
var data = line.Trim("data: "); var data = line.Trim("data: ");
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.TunedOptions); var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
yield return deserializedData; yield return deserializedData;
@@ -100,14 +99,14 @@ public class ContainerHelperService
var response = await client.PostAsJsonAsync( var response = await client.PostAsJsonAsync(
"api/configuration/version", "api/configuration/version",
new SetVersionDto(version), new SetVersionDto(version),
SerializationContext.TunedOptions SerializationContext.Default.Options
); );
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
return; return;
var problemDetails = var problemDetails =
await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.TunedOptions); await response.Content.ReadFromJsonAsync<ProblemDetails>(SerializationContext.Default.Options);
if (problemDetails == null) if (problemDetails == null)
throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}"); throw new HttpRequestException($"Failed to set version: {response.ReasonPhrase}");

View File

@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Moonlight.Api.Configuration; using Moonlight.Api.Configuration;
using Moonlight.Api.Constants;
using Moonlight.Api.Database; using Moonlight.Api.Database;
using Moonlight.Api.Database.Entities; using Moonlight.Api.Database.Entities;
using Moonlight.Api.Models; using Moonlight.Api.Models;
@@ -13,14 +14,16 @@ public class FrontendService
private readonly IMemoryCache Cache; private readonly IMemoryCache Cache;
private readonly DatabaseRepository<Theme> ThemeRepository; private readonly DatabaseRepository<Theme> ThemeRepository;
private readonly IOptions<FrontendOptions> Options; private readonly IOptions<FrontendOptions> Options;
private readonly SettingsService SettingsService;
private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}"; private const string CacheKey = $"Moonlight.{nameof(FrontendService)}.{nameof(GetConfigurationAsync)}";
public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options) public FrontendService(IMemoryCache cache, DatabaseRepository<Theme> themeRepository, IOptions<FrontendOptions> options, SettingsService settingsService)
{ {
Cache = cache; Cache = cache;
ThemeRepository = themeRepository; ThemeRepository = themeRepository;
Options = options; Options = options;
SettingsService = settingsService;
} }
public async Task<FrontendConfiguration> GetConfigurationAsync() public async Task<FrontendConfiguration> GetConfigurationAsync()
@@ -35,7 +38,9 @@ public class FrontendService
.Query() .Query()
.FirstOrDefaultAsync(x => x.IsEnabled); .FirstOrDefaultAsync(x => x.IsEnabled);
var config = new FrontendConfiguration("Moonlight", theme?.CssContent); var name = await SettingsService.GetValueAsync<string>(FrontendSettingConstants.Name);
var config = new FrontendConfiguration(name ?? "Moonlight", theme?.CssContent);
Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes)); Cache.Set(CacheKey, config, TimeSpan.FromMinutes(Options.Value.CacheMinutes));

View File

@@ -1,11 +0,0 @@
using Microsoft.AspNetCore.Builder;
using SimplePlugin.Abstractions;
namespace Moonlight.Api.Startup;
public interface IAppStartup : IPluginModule
{
public void PreBuild(WebApplicationBuilder builder);
public void PostBuild(WebApplication application);
public void PostMiddleware(WebApplication application);
}

View File

@@ -16,13 +16,16 @@ namespace Moonlight.Api.Startup;
public partial class Startup public partial class Startup
{ {
private static void AddBase(WebApplicationBuilder builder) private void AddBase(WebApplicationBuilder builder)
{ {
// Create the base directory // Create the base directory
Directory.CreateDirectory("storage"); Directory.CreateDirectory("storage");
// Hook up source-generated serialization // Hook up source-generated serialization and add controllers
builder.Services.AddControllers().AddJsonOptions(options => builder.Services
.AddControllers()
.AddApplicationPart(typeof(Startup).Assembly)
.AddJsonOptions(options =>
{ {
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default); options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
}); });

View File

@@ -1,12 +1,15 @@
using Microsoft.AspNetCore.Builder; using System.Reflection;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.Builder;
using Moonlight.Shared.Http;
using SimplePlugin.Abstractions; using SimplePlugin.Abstractions;
namespace Moonlight.Api.Startup; namespace Moonlight.Api.Startup;
[PluginModule] [PluginModule]
public partial class Startup : IAppStartup public partial class Startup : MoonlightPlugin
{ {
public void PreBuild(WebApplicationBuilder builder) public override void PreBuild(WebApplicationBuilder builder)
{ {
AddBase(builder); AddBase(builder);
AddAuth(builder); AddAuth(builder);
@@ -14,13 +17,13 @@ public partial class Startup : IAppStartup
AddCache(builder); AddCache(builder);
} }
public void PostBuild(WebApplication application) public override void PostBuild(WebApplication application)
{ {
UseBase(application); UseBase(application);
UseAuth(application); UseAuth(application);
} }
public void PostMiddleware(WebApplication application) public override void PostMiddleware(WebApplication application)
{ {
MapBase(application); MapBase(application);
} }

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Hosting;
namespace Moonlight.Api;
public static class StartupHandler
{
public static async Task RunAsync(string[] args, MoonlightPlugin[] plugins)
{
Console.WriteLine($"Starting with: {string.Join(", ", plugins.Select(x => x.GetType().FullName))}");
var builder = WebApplication.CreateBuilder(args);
// Setting up context
foreach (var plugin in plugins)
plugin.Initialize(plugins);
// Stage 1: Pre Build
foreach (var startup in plugins)
startup.PreBuild(builder);
var app = builder.Build();
// Stage 2: Post Build
foreach (var startup in plugins)
startup.PostBuild(app);
// Stage 3: Post Middleware
foreach (var startup in plugins)
startup.PostMiddleware(app);
// Frontend debugging
if (app.Environment.IsDevelopment())
app.UseWebAssemblyDebugging();
// Frontend hosting
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();
await app.RunAsync();
}
}

View File

@@ -0,0 +1,26 @@
using System.Diagnostics.CodeAnalysis;
using Moonlight.Frontend.Interfaces;
namespace Moonlight.Frontend.Configuration;
public class LayoutMiddlewareOptions
{
public IReadOnlyList<Type> Components => InnerComponents;
private readonly List<Type> InnerComponents = new();
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : LayoutMiddlewareBase
{
InnerComponents.Add(typeof(T));
}
public void Insert<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(int index) where T : LayoutMiddlewareBase
{
InnerComponents.Insert(index, typeof(T));
}
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>() where T : LayoutMiddlewareBase
{
InnerComponents.Remove(typeof(T));
}
}

View File

@@ -0,0 +1,33 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Configuration;
public class LayoutPageOptions
{
public IReadOnlyList<LayoutPageComponent> Components => InnerComponents;
private readonly List<LayoutPageComponent> InnerComponents = new();
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>(LayoutPageSlot slot, int order)
where T : ComponentBase
=> Add(typeof(T), slot, order);
public void Add(Type componentType, LayoutPageSlot slot, int order)
=> InnerComponents.Add(new LayoutPageComponent(componentType, order, slot));
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] T>()
where T : ComponentBase
=> Remove(typeof(T));
public void Remove(Type componentType)
=> InnerComponents.RemoveAll(x => x.ComponentType == componentType);
}
public record LayoutPageComponent(Type ComponentType, int Order, LayoutPageSlot Slot);
public enum LayoutPageSlot
{
Header = 0,
Footer = 1
}

View File

@@ -0,0 +1,8 @@
using System.Reflection;
namespace Moonlight.Frontend.Configuration;
public class NavigationAssemblyOptions
{
public List<Assembly> Assemblies { get; private set; } = new();
}

View File

@@ -0,0 +1,35 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Configuration;
public class SystemSettingsOptions
{
public IReadOnlyList<SystemSettingsPage> Components => InnerComponents;
private readonly List<SystemSettingsPage> InnerComponents = new();
public void Add<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TIcon,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(string name, string description,
int order)
where TIcon : ComponentBase where TComponent : ComponentBase
=> Add(name, description, order, typeof(TIcon), typeof(TComponent));
public void Add(string name, string description, int order, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type iconComponent, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type component)
=> InnerComponents.Add(new SystemSettingsPage(name, description, order, iconComponent, component));
public void Remove<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>()
where TComponent : ComponentBase
=> Remove(typeof(TComponent));
public void Remove([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
=> InnerComponents.RemoveAll(x => x.ComponentType == componentType);
}
public record SystemSettingsPage(
string Name,
string Description,
int Order,
Type IconComponentType,
Type ComponentType
);

View File

@@ -1,28 +0,0 @@
using System.Text.Json;
using Moonlight.Shared.Http;
namespace Moonlight.Frontend;
public static class Constants
{
public static JsonSerializerOptions SerializerOptions
{
get
{
if (InternalOptions != null)
return InternalOptions;
InternalOptions = new()
{
PropertyNameCaseInsensitive = true
};
// Add source generated options from shared project
InternalOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
return InternalOptions;
}
}
private static JsonSerializerOptions? InternalOptions;
}

View File

@@ -29,6 +29,7 @@ public sealed class PermissionProvider : IPermissionProvider
new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"), new Permission(Permissions.System.Diagnose, "Diagnose", "Run diagnostics"),
new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"), new Permission(Permissions.System.Versions, "Versions", "Look at the available versions"),
new Permission(Permissions.System.Instance, "Instance", "Update the moonlight instance and add plugins"), new Permission(Permissions.System.Instance, "Instance", "Update the moonlight instance and add plugins"),
new Permission(Permissions.System.Settings, "Settings", "Change settings of the instance"),
]), ]),
new PermissionCategory("API Keys", typeof(KeyIcon), [ new PermissionCategory("API Keys", typeof(KeyIcon), [
new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"), new Permission(Permissions.ApiKeys.Create, "Create", "Create new API keys"),

View File

@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace Moonlight.Frontend.Interfaces;
public abstract class LayoutMiddlewareBase : ComponentBase
{
[Parameter] public RenderFragment ChildContent { get; set; }
}

View File

@@ -24,8 +24,8 @@
<PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/> <PackageReference Include="Riok.Mapperly" Version="4.3.1-next.0"/>
<PackageReference Include="ShadcnBlazor" Version="1.0.11" /> <PackageReference Include="ShadcnBlazor" Version="1.0.13" />
<PackageReference Include="ShadcnBlazor.Extras" Version="1.0.11" /> <PackageReference Include="ShadcnBlazor.Extras" Version="1.0.13" />
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" /> <PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
</ItemGroup> </ItemGroup>
@@ -36,5 +36,6 @@
<ItemGroup> <ItemGroup>
<None Include="Styles/*" Pack="true" PackagePath="Styles/" /> <None Include="Styles/*" Pack="true" PackagePath="Styles/" />
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" /> <None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\Moonlight.Frontend.targets" />
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="buildTransitive\Moonlight.Frontend.targets" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -5,7 +5,7 @@
</MoonlightCssClassDir> </MoonlightCssClassDir>
</PropertyGroup> </PropertyGroup>
<Target Name="CopyContents" BeforeTargets="Build"> <Target Name="Moonlight_CopyContents" BeforeTargets="Build">
<ItemGroup> <ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" /> <Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
</ItemGroup> </ItemGroup>

View File

@@ -0,0 +1,17 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using SimplePlugin.Abstractions;
namespace Moonlight.Frontend;
public abstract class MoonlightPlugin : IPluginModule
{
protected MoonlightPlugin[] Plugins { get; private set; }
public void Initialize(MoonlightPlugin[] plugins)
{
Plugins = plugins;
}
public virtual void PreBuild(WebAssemblyHostBuilder builder){}
public virtual void PostBuild(WebAssemblyHost application){}
}

View File

@@ -3,6 +3,8 @@ using System.Net.Http.Json;
using System.Security.Claims; using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.VisualBasic;
using Moonlight.Shared.Http;
using Moonlight.Shared.Http.Responses.Admin.Auth; using Moonlight.Shared.Http.Responses.Admin.Auth;
namespace Moonlight.Frontend.Services; namespace Moonlight.Frontend.Services;
@@ -23,7 +25,7 @@ public class RemoteAuthProvider : AuthenticationStateProvider
try try
{ {
var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>( var claimResponses = await HttpClient.GetFromJsonAsync<ClaimDto[]>(
"api/auth/claims", Constants.SerializerOptions "api/auth/claims", SerializationContext.Default.Options
); );
var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value)); var claims = claimResponses!.Select(claim => new Claim(claim.Type, claim.Value));

View File

@@ -1,10 +0,0 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using SimplePlugin.Abstractions;
namespace Moonlight.Frontend.Startup;
public interface IAppStartup : IPluginModule
{
public void PreBuild(WebAssemblyHostBuilder builder);
public void PostBuild(WebAssemblyHost application);
}

View File

@@ -1,10 +1,13 @@
using LucideBlazor;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Moonlight.Frontend.Configuration;
using Moonlight.Frontend.Implementations; using Moonlight.Frontend.Implementations;
using Moonlight.Frontend.Interfaces; using Moonlight.Frontend.Interfaces;
using Moonlight.Frontend.Services; using Moonlight.Frontend.Services;
using Moonlight.Frontend.UI; using Moonlight.Frontend.UI;
using Moonlight.Frontend.UI.Admin.Settings;
using ShadcnBlazor; using ShadcnBlazor;
using ShadcnBlazor.Extras; using ShadcnBlazor.Extras;
@@ -25,5 +28,19 @@ public partial class Startup
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>(); builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
builder.Services.AddScoped<FrontendService>(); builder.Services.AddScoped<FrontendService>();
builder.Services.Configure<NavigationAssemblyOptions>(options =>
{
options.Assemblies.Add(typeof(Startup).Assembly);
});
builder.Services.Configure<SystemSettingsOptions>(options =>
{
options.Add<TextCursorInputIcon, WhiteLabelingSetting>(
"White Labeling",
"Settings for white labeling your moonlight instance",
0
);
});
} }
} }

View File

@@ -4,15 +4,15 @@ using SimplePlugin.Abstractions;
namespace Moonlight.Frontend.Startup; namespace Moonlight.Frontend.Startup;
[PluginModule] [PluginModule]
public partial class Startup : IAppStartup public partial class Startup : MoonlightPlugin
{ {
public void PreBuild(WebAssemblyHostBuilder builder) public override void PreBuild(WebAssemblyHostBuilder builder)
{ {
AddBase(builder); AddBase(builder);
AddAuth(builder); AddAuth(builder);
} }
public void PostBuild(WebAssemblyHost application) public override void PostBuild(WebAssemblyHost application)
{ {
} }

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
namespace Moonlight.Frontend;
public static class StartupHandler
{
public static async Task RunAsync(string[] args, MoonlightPlugin[] plugins)
{
Console.WriteLine($"Starting with: {string.Join(", ", plugins.Select(x => x.GetType().FullName))}");
var builder = WebAssemblyHostBuilder.CreateDefault(args);
// Setting up context
foreach (var plugin in plugins)
plugin.Initialize(plugins);
// Stage 1: Pre Build
foreach (var plugin in plugins)
plugin.PreBuild(builder);
var app = builder.Build();
// Stage 2: Post Build
foreach(var plugin in plugins)
plugin.PostBuild(app);
await app.RunAsync();
}
}

View File

@@ -1,5 +1,6 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys @using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@@ -77,7 +78,7 @@
var response = await HttpClient.PostAsJsonAsync( var response = await HttpClient.PostAsJsonAsync(
"/api/admin/apiKeys", "/api/admin/apiKeys",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -1,5 +1,6 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Roles @using Moonlight.Shared.Http.Requests.Admin.Roles
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.Forms @using ShadcnBlazor.Extras.Forms
@@ -76,7 +77,7 @@
var response = await HttpClient.PostAsJsonAsync( var response = await HttpClient.PostAsJsonAsync(
"api/admin/roles", "api/admin/roles",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -1,4 +1,5 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Users @using Moonlight.Shared.Http.Requests.Admin.Users
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@@ -65,7 +66,7 @@
var response = await HttpClient.PostAsJsonAsync( var response = await HttpClient.PostAsJsonAsync(
"/api/admin/users", "/api/admin/users",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -1,6 +1,7 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.ApiKeys @using Moonlight.Shared.Http.Requests.Admin.ApiKeys
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys @using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@@ -74,7 +75,7 @@
var response = await HttpClient.PatchAsJsonAsync( var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/apiKeys/{Key.Id}", $"/api/admin/apiKeys/{Key.Id}",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -124,7 +124,7 @@ else
await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto() await HttpClient.PostAsJsonAsync("api/admin/ch/version", new SetVersionDto()
{ {
Version = Version Version = Version
}, SerializationContext.TunedOptions); }, SerializationContext.Default.Options);
// Starting rebuild task // Starting rebuild task
CurrentStep = 2; CurrentStep = 2;
@@ -136,7 +136,7 @@ else
request.Content = JsonContent.Create( request.Content = JsonContent.Create(
new RequestRebuildDto(NoBuildCache), new RequestRebuildDto(NoBuildCache),
null, null,
SerializationContext.TunedOptions SerializationContext.Default.Options
); );
var response = await HttpClient.SendAsync( var response = await HttpClient.SendAsync(
@@ -160,7 +160,7 @@ else
continue; continue;
var data = line.Trim("data: "); var data = line.Trim("data: ");
var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, Constants.SerializerOptions); var deserializedData = JsonSerializer.Deserialize<RebuildEventDto>(data, SerializationContext.Default.Options);
switch (deserializedData.Type) switch (deserializedData.Type)
{ {

View File

@@ -1,6 +1,7 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.UI.Admin.Components @using Moonlight.Frontend.UI.Admin.Components
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Roles @using Moonlight.Shared.Http.Requests.Admin.Roles
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Dialogs @using ShadcnBlazor.Dialogs
@@ -75,7 +76,7 @@
var response = await HttpClient.PatchAsJsonAsync( var response = await HttpClient.PatchAsJsonAsync(
$"api/admin/roles/{Role.Id}", $"api/admin/roles/{Role.Id}",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -0,0 +1,75 @@
@using LucideBlazor
@using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Services
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Settings
@using Moonlight.Shared.Http.Responses.Admin.Settings
@using ShadcnBlazor.Extras.Common
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@inject HttpClient HttpClient
@inject ToastService ToastService
@inject FrontendService FrontendService
<LazyLoader Load="LoadAsync">
<EnhancedEditForm Model="Request" OnValidSubmit="OnValidSubmit">
<DataAnnotationsValidator />
<FieldSet>
<FormValidationSummary />
<FieldGroup>
<Field>
<FieldLabel>Name</FieldLabel>
<TextInputField @bind-Value="Request.Name"/>
</Field>
</FieldGroup>
</FieldSet>
<SubmitButton ClassName="mt-3">
<SaveIcon/>
Save
</SubmitButton>
</EnhancedEditForm>
</LazyLoader>
@code
{
private SetWhiteLabelingDto Request;
private async Task LoadAsync(LazyLoader _)
{
var dto = await HttpClient.GetFromJsonAsync<WhiteLabelingDto>(
"api/admin/system/settings/whiteLabeling",
SerializationContext.Default.Options
);
Request = new SetWhiteLabelingDto()
{
Name = dto!.Name
};
}
private async Task<bool> OnValidSubmit(EditContext editContext, ValidationMessageStore validationMessageStore)
{
var response = await HttpClient.PostAsJsonAsync(
"api/admin/system/settings/whiteLabeling",
Request,
SerializationContext.Default.Options
);
if (response.IsSuccessStatusCode)
{
await FrontendService.ReloadAsync();
await ToastService.SuccessAsync("Setting", "Successfully updated white labeling settings");
return true;
}
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Request, validationMessageStore);
return false;
}
}

View File

@@ -1,6 +1,7 @@
@page "/admin" @page "/admin"
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards @using ShadcnBlazor.Cards
@@ -155,7 +156,7 @@
if(!firstRender) if(!firstRender)
return; return;
InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoDto>("api/admin/system/info", Constants.SerializerOptions); InfoResponse = await HttpClient.GetFromJsonAsync<SystemInfoDto>("api/admin/system/info", SerializationContext.Default.Options);
IsInfoLoading = false; IsInfoLoading = false;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.ApiKeys @using Moonlight.Shared.Http.Responses.Admin.ApiKeys
@@ -126,7 +127,7 @@
var response = await HttpClient.GetFromJsonAsync<PagedData<ApiKeyDto>>( var response = await HttpClient.GetFromJsonAsync<PagedData<ApiKeyDto>>(
$"api/admin/apiKeys{query}&filterOptions={filterOptions}", $"api/admin/apiKeys{query}&filterOptions={filterOptions}",
Constants.SerializerOptions SerializationContext.Default.Options
); );
return new DataGridResponse<ApiKeyDto>(response!.Data, response.TotalLength); return new DataGridResponse<ApiKeyDto>(response!.Data, response.TotalLength);

View File

@@ -4,18 +4,14 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Shared @using Moonlight.Shared
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Inputs
@using ShadcnBlazor.Tab @using ShadcnBlazor.Tab
@using ShadcnBlazor.Labels
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IAuthorizationService AuthorizationService @inject IAuthorizationService AuthorizationService
<Tabs DefaultValue="@(Tab ?? "settings")" OnValueChanged="OnTabChanged"> <Tabs DefaultValue="@(Tab ?? "settings")" OnValueChanged="OnTabChanged">
<TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden"> <TabsList ClassName="inline-flex w-full lg:w-fit justify-start overflow-x-auto overflow-y-hidden">
<TabsTrigger Value="settings"> <TabsTrigger Value="settings" Disabled="@(!SettingsResult.Succeeded)">
<CogIcon /> <CogIcon />
Settings Settings
</TabsTrigger> </TabsTrigger>
@@ -27,7 +23,7 @@
<KeyIcon/> <KeyIcon/>
API & API Keys API & API Keys
</TabsTrigger> </TabsTrigger>
<TabsTrigger Value="diagnose"> <TabsTrigger Value="diagnose" Disabled="@(!DiagnoseResult.Succeeded)">
<HeartPulseIcon/> <HeartPulseIcon/>
Diagnose Diagnose
</TabsTrigger> </TabsTrigger>
@@ -36,19 +32,18 @@
Instance Instance
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@if (SettingsResult.Succeeded)
{
<TabsContent Value="settings"> <TabsContent Value="settings">
<Card ClassName="mt-5"> <Settings />
<CardFooter ClassName="justify-end">
<Button>
<SaveIcon />
Save changes
</Button>
</CardFooter>
</Card>
</TabsContent> </TabsContent>
}
@if (DiagnoseResult.Succeeded)
{
<TabsContent Value="diagnose"> <TabsContent Value="diagnose">
<Diagnose /> <Diagnose />
</TabsContent> </TabsContent>
}
@if (ApiKeyAccess.Succeeded) @if (ApiKeyAccess.Succeeded)
{ {
<TabsContent Value="apiKeys"> <TabsContent Value="apiKeys">
@@ -81,6 +76,8 @@
private AuthorizationResult ThemesAccess; private AuthorizationResult ThemesAccess;
private AuthorizationResult InstanceResult; private AuthorizationResult InstanceResult;
private AuthorizationResult VersionsResult; private AuthorizationResult VersionsResult;
private AuthorizationResult SettingsResult;
private AuthorizationResult DiagnoseResult;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@@ -90,6 +87,8 @@
ThemesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.View); ThemesAccess = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.Themes.View);
InstanceResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Versions); InstanceResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Versions);
VersionsResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Instance); VersionsResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Instance);
SettingsResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Settings);
DiagnoseResult = await AuthorizationService.AuthorizeAsync(authState.User, Permissions.System.Diagnose);
} }
private void OnTabChanged(string name) private void OnTabChanged(string name)

View File

@@ -0,0 +1,52 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Configuration
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Sidebars
@inject IOptions<SystemSettingsOptions> Options
<div class="mt-5 flex flex-col md:flex-row gap-5">
<Card ClassName="flex py-2 grow-0 min-w-56 max-h-[65vh] md:min-h-[65vh]">
<CardContent ClassName="px-2 flex flex-col gap-y-1 h-full max-h-[65vh] overflow-y-auto scrollbar-thin">
@foreach (var menuPage in Pages)
{
<SidebarMenuButton @onclick="() => Navigate(menuPage)" IsActive="@(CurrentPage == menuPage)" ClassName="overflow-visible">
<DynamicComponent Type="@menuPage.IconComponentType" />
<span>@menuPage.Name</span>
</SidebarMenuButton>
}
</CardContent>
</Card>
@if (CurrentPage != null)
{
<Card ClassName="flex grow">
<CardHeader>
<CardTitle>@CurrentPage.Name</CardTitle>
<CardDescription>@CurrentPage.Description</CardDescription>
</CardHeader>
<CardContent>
<DynamicComponent Type="@CurrentPage.ComponentType" />
</CardContent>
</Card>
}
</div>
@code
{
private SystemSettingsPage[] Pages;
private SystemSettingsPage? CurrentPage;
protected override void OnInitialized()
{
Pages = Options
.Value
.Components
.OrderBy(x => x.Order)
.ToArray();
CurrentPage = Pages.FirstOrDefault();
}
private void Navigate(SystemSettingsPage page)
=> CurrentPage = page;
}

View File

@@ -5,6 +5,7 @@
@using LucideBlazor @using LucideBlazor
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Services @using Moonlight.Frontend.Services
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Themes @using Moonlight.Shared.Http.Requests.Admin.Themes
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards @using ShadcnBlazor.Cards
@@ -122,7 +123,7 @@
var response = await HttpClient.PostAsJsonAsync( var response = await HttpClient.PostAsJsonAsync(
"/api/admin/themes", "/api/admin/themes",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -2,6 +2,7 @@
@using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin.Themes @using Moonlight.Shared.Http.Responses.Admin.Themes
@@ -136,7 +137,7 @@
var response = await HttpClient.GetFromJsonAsync<PagedData<ThemeDto>>( var response = await HttpClient.GetFromJsonAsync<PagedData<ThemeDto>>(
$"api/admin/themes{query}&filterOptions={filterOptions}", $"api/admin/themes{query}&filterOptions={filterOptions}",
Constants.SerializerOptions SerializationContext.Default.Options
); );
return new DataGridResponse<ThemeDto>(response!.Data, response.TotalLength); return new DataGridResponse<ThemeDto>(response!.Data, response.TotalLength);
@@ -182,7 +183,7 @@
var importedTheme = await response var importedTheme = await response
.Content .Content
.ReadFromJsonAsync<ThemeDto>(Constants.SerializerOptions); .ReadFromJsonAsync<ThemeDto>(SerializationContext.Default.Options);
if (importedTheme == null) if (importedTheme == null)
continue; continue;

View File

@@ -6,6 +6,7 @@
@using Moonlight.Frontend.Helpers @using Moonlight.Frontend.Helpers
@using Moonlight.Frontend.Mappers @using Moonlight.Frontend.Mappers
@using Moonlight.Frontend.Services @using Moonlight.Frontend.Services
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests.Admin.Themes @using Moonlight.Shared.Http.Requests.Admin.Themes
@using Moonlight.Shared.Http.Responses.Admin.Themes @using Moonlight.Shared.Http.Responses.Admin.Themes
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@@ -136,7 +137,7 @@
var response = await HttpClient.PatchAsJsonAsync( var response = await HttpClient.PatchAsJsonAsync(
$"/api/admin/themes/{Theme.Id}", $"/api/admin/themes/{Theme.Id}",
Request, Request,
Constants.SerializerOptions SerializationContext.Default.Options
); );
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Requests @using Moonlight.Shared.Http.Requests
@using Moonlight.Shared.Http.Responses @using Moonlight.Shared.Http.Responses
@using Moonlight.Shared.Http.Responses.Admin @using Moonlight.Shared.Http.Responses.Admin
@@ -123,7 +124,7 @@
var response = await HttpClient.GetFromJsonAsync<PagedData<RoleDto>>( var response = await HttpClient.GetFromJsonAsync<PagedData<RoleDto>>(
$"api/admin/roles{query}&filterOptions={filterOptions}", $"api/admin/roles{query}&filterOptions={filterOptions}",
Constants.SerializerOptions SerializationContext.Default.Options
); );
return new DataGridResponse<RoleDto>(response!.Data, response.TotalLength); return new DataGridResponse<RoleDto>(response!.Data, response.TotalLength);

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.UI.Admin.Modals @using Moonlight.Frontend.UI.Admin.Modals
@using Moonlight.Shared @using Moonlight.Shared
@using Moonlight.Shared.Http
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@using ShadcnBlazor.DataGrids @using ShadcnBlazor.DataGrids
@using ShadcnBlazor.Dropdowns @using ShadcnBlazor.Dropdowns
@@ -121,7 +122,7 @@
var response = await HttpClient.GetFromJsonAsync<PagedData<UserDto>>( var response = await HttpClient.GetFromJsonAsync<PagedData<UserDto>>(
$"api/admin/users{query}&filterOptions={filterOptions}", $"api/admin/users{query}&filterOptions={filterOptions}",
Constants.SerializerOptions SerializationContext.Default.Options
); );
return new DataGridResponse<UserDto>(response!.Data, response.TotalLength); return new DataGridResponse<UserDto>(response!.Data, response.TotalLength);

View File

@@ -1,19 +1,28 @@
@using System.Net @using System.Net
@using System.Reflection
@using LucideBlazor @using LucideBlazor
@using Microsoft.AspNetCore.Components.Authorization @using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Configuration
@using Moonlight.Frontend.UI.Shared @using Moonlight.Frontend.UI.Shared
@using Moonlight.Frontend.UI.Shared.Components @using Moonlight.Frontend.UI.Shared.Components
@using ShadcnBlazor.Emptys @using ShadcnBlazor.Emptys
@using Moonlight.Frontend.UI.Shared.Components.Auth @using Moonlight.Frontend.UI.Shared.Components.Auth
@using Moonlight.Frontend.UI.Shared.Partials @using Moonlight.Frontend.UI.Shared.Partials
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Portals
@inject NavigationManager Navigation @inject NavigationManager Navigation
@inject IOptions<NavigationAssemblyOptions> NavigationOptions
<ErrorBoundary> <ErrorBoundary>
<ChildContent> <ChildContent>
<AuthorizeView> <AuthorizeView>
<ChildContent> <ChildContent>
<Router AppAssembly="@typeof(App).Assembly" NotFoundPage="typeof(NotFound)"> <LayoutMiddleware>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="Assemblies" NotFoundPage="typeof(NotFound)">
<Found Context="routeData"> <Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)"> <AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
<NotAuthorized Context="authRouteViewContext"> <NotAuthorized Context="authRouteViewContext">
@@ -22,6 +31,13 @@
</AuthorizeRouteView> </AuthorizeRouteView>
</Found> </Found>
</Router> </Router>
</LayoutMiddleware>
<ToastLauncher/>
<DialogLauncher/>
<AlertDialogLauncher/>
<PortalOutlet />
</ChildContent> </ChildContent>
<Authorizing> <Authorizing>
<Authenticating/> <Authenticating/>
@@ -72,3 +88,13 @@
} }
</ErrorContent> </ErrorContent>
</ErrorBoundary> </ErrorBoundary>
@code
{
private Assembly[] Assemblies;
protected override void OnInitialized()
{
Assemblies = NavigationOptions.Value.Assemblies.ToArray();
}
}

View File

@@ -1,4 +1,5 @@
@using Moonlight.Shared.Http.Responses.Admin.Auth @using Moonlight.Shared.Http
@using Moonlight.Shared.Http.Responses.Admin.Auth
@using ShadcnBlazor.Cards @using ShadcnBlazor.Cards
@using ShadcnBlazor.Spinners @using ShadcnBlazor.Spinners
@using ShadcnBlazor.Buttons @using ShadcnBlazor.Buttons
@@ -48,7 +49,7 @@
return; return;
var schemes = await HttpClient.GetFromJsonAsync<SchemeDto[]>( var schemes = await HttpClient.GetFromJsonAsync<SchemeDto[]>(
"api/auth", Constants.SerializerOptions "api/auth", SerializationContext.Default.Options
); );
if (schemes == null) if (schemes == null)

View File

@@ -0,0 +1,34 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Configuration
@using Moonlight.Frontend.Interfaces
@inject IOptions<LayoutMiddlewareOptions> Options
@Chain
@code
{
[Parameter] public RenderFragment ChildContent { get; set; }
private RenderFragment Chain;
protected override void OnInitialized()
{
Chain = ChildContent;
foreach (var component in Options.Value.Components)
{
// Capture current values
var currentChain = Chain;
var currentComponent = component;
Chain = builder =>
{
builder.OpenComponent(0, currentComponent);
builder.SetKey(component);
builder.AddComponentParameter(1, nameof(LayoutMiddlewareBase.ChildContent), currentChain);
builder.CloseComponent();
};
}
}
}

View File

@@ -1,28 +1,53 @@
@using ShadcnBlazor.Extras.AlertDialogs @using Microsoft.Extensions.Options
@using Moonlight.Frontend.Configuration
@using ShadcnBlazor.Extras.Alerts @using ShadcnBlazor.Extras.Alerts
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Portals
@using ShadcnBlazor.Sidebars @using ShadcnBlazor.Sidebars
@inherits LayoutComponentBase @inherits LayoutComponentBase
@inject IOptions<LayoutPageOptions> LayoutPageOptions
<SidebarProvider DefaultOpen="true"> <SidebarProvider DefaultOpen="true">
<AppSidebar/> <AppSidebar/>
<SidebarInset> <SidebarInset>
<AppHeader/> <AppHeader/>
@foreach (var headerComponent in HeaderComponents)
{
<DynamicComponent Type="headerComponent" />
}
<div class="mx-8 my-8 max-w-full"> <div class="mx-8 my-8 max-w-full">
<AlertLauncher/> <AlertLauncher/>
@Body @Body
</div> </div>
<ToastLauncher/> @foreach (var footerComponent in FooterComponents)
<DialogLauncher/> {
<AlertDialogLauncher/> <DynamicComponent Type="footerComponent" />
}
<PortalOutlet />
</SidebarInset> </SidebarInset>
</SidebarProvider> </SidebarProvider>
@code
{
private Type[] HeaderComponents;
private Type[] FooterComponents;
protected override void OnInitialized()
{
HeaderComponents = LayoutPageOptions.Value.Components
.Where(x => x.Slot == LayoutPageSlot.Header)
.OrderBy(x => x.Order)
.Select(x => x.ComponentType)
.ToArray();
FooterComponents = LayoutPageOptions.Value.Components
.Where(x => x.Slot == LayoutPageSlot.Footer)
.OrderBy(x => x.Order)
.Select(x => x.ComponentType)
.ToArray();
}
}

View File

@@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Moonlight.Shared.Http.Requests.Admin.Settings;
public class SetWhiteLabelingDto
{
[Required]
public string Name { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Moonlight.Shared.Http.Responses.Admin.Settings;
public class WhiteLabelingDto
{
public string Name { get; set; }
}

View File

@@ -4,12 +4,14 @@ using Moonlight.Shared.Http.Events;
using Moonlight.Shared.Http.Requests.Admin.ApiKeys; using Moonlight.Shared.Http.Requests.Admin.ApiKeys;
using Moonlight.Shared.Http.Requests.Admin.ContainerHelper; using Moonlight.Shared.Http.Requests.Admin.ContainerHelper;
using Moonlight.Shared.Http.Requests.Admin.Roles; using Moonlight.Shared.Http.Requests.Admin.Roles;
using Moonlight.Shared.Http.Requests.Admin.Settings;
using Moonlight.Shared.Http.Requests.Admin.Themes; using Moonlight.Shared.Http.Requests.Admin.Themes;
using Moonlight.Shared.Http.Requests.Admin.Users; using Moonlight.Shared.Http.Requests.Admin.Users;
using Moonlight.Shared.Http.Responses; using Moonlight.Shared.Http.Responses;
using Moonlight.Shared.Http.Responses.Admin; using Moonlight.Shared.Http.Responses.Admin;
using Moonlight.Shared.Http.Responses.Admin.ApiKeys; using Moonlight.Shared.Http.Responses.Admin.ApiKeys;
using Moonlight.Shared.Http.Responses.Admin.Auth; using Moonlight.Shared.Http.Responses.Admin.Auth;
using Moonlight.Shared.Http.Responses.Admin.Settings;
using Moonlight.Shared.Http.Responses.Admin.Themes; using Moonlight.Shared.Http.Responses.Admin.Themes;
using Moonlight.Shared.Http.Responses.Admin.Users; using Moonlight.Shared.Http.Responses.Admin.Users;
@@ -58,21 +60,13 @@ namespace Moonlight.Shared.Http;
//Misc //Misc
[JsonSerializable(typeof(VersionDto))] [JsonSerializable(typeof(VersionDto))]
[JsonSerializable(typeof(ProblemDetails))] [JsonSerializable(typeof(ProblemDetails))]
// Settings - White Labeling
[JsonSerializable(typeof(WhiteLabelingDto))]
[JsonSerializable(typeof(SetWhiteLabelingDto))]
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
public partial class SerializationContext : JsonSerializerContext public partial class SerializationContext : JsonSerializerContext
{ {
private static JsonSerializerOptions? InternalTunedOptions;
public static JsonSerializerOptions TunedOptions
{
get
{
if (InternalTunedOptions != null)
return InternalTunedOptions;
InternalTunedOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);
InternalTunedOptions.TypeInfoResolverChain.Add(Default);
return InternalTunedOptions;
}
}
} }

View File

@@ -55,5 +55,6 @@ public static class Permissions
public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}"; public const string Diagnose = $"{Prefix}{Section}.{nameof(Diagnose)}";
public const string Versions = $"{Prefix}{Section}.{nameof(Versions)}"; public const string Versions = $"{Prefix}{Section}.{nameof(Versions)}";
public const string Instance = $"{Prefix}{Section}.{nameof(Instance)}"; public const string Instance = $"{Prefix}{Section}.{nameof(Instance)}";
public const string Settings = $"{Prefix}{Section}.{nameof(Settings)}";
} }
} }