Refactored project to module structure

This commit is contained in:
2026-03-12 22:50:15 +01:00
parent 93de9c5d00
commit 1257e8b950
219 changed files with 1231 additions and 1259 deletions

View File

@@ -0,0 +1,97 @@
@using System.Net
@using System.Reflection
@using LucideBlazor
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Infrastructure.Configuration
@using Moonlight.Frontend.Shared.Auth
@using ShadcnBlazor.Emptys
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Dialogs
@using ShadcnBlazor.Extras.Toasts
@using ShadcnBlazor.Portals
@inject NavigationManager Navigation
@inject IOptions<NavigationAssemblyOptions> NavigationOptions
<ErrorBoundary>
<ChildContent>
<AuthorizeView>
<ChildContent>
<LayoutMiddleware>
<Router AppAssembly="@typeof(App).Assembly" AdditionalAssemblies="Assemblies"
NotFoundPage="typeof(NotFound)">
<Found Context="routeData">
<AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)">
<NotAuthorized Context="authRouteViewContext">
<AccessDenied/>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
</Router>
</LayoutMiddleware>
<ToastLauncher/>
<DialogLauncher/>
<AlertDialogLauncher/>
<PortalOutlet/>
</ChildContent>
<Authorizing>
<Authenticating/>
</Authorizing>
<NotAuthorized>
@if (context.User.Identity?.IsAuthenticated ?? false)
{
<AccessDenied/>
}
else
{
var uri = new Uri(Navigation.Uri);
if (uri.LocalPath.StartsWith("/setup"))
{
<Setup/>
}
else
{
<Authentication/>
}
}
</NotAuthorized>
</AuthorizeView>
</ChildContent>
<ErrorContent>
@if (context is HttpRequestException { StatusCode: HttpStatusCode.Unauthorized })
{
<Authentication/>
}
else
{
<div class="m-10">
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<OctagonAlertIcon ClassName="text-red-500/80"/>
</EmptyMedia>
<EmptyTitle>
Critical Application Error
</EmptyTitle>
<EmptyDescription>
@context.ToString()
</EmptyDescription>
</EmptyHeader>
</Empty>
</div>
}
</ErrorContent>
</ErrorBoundary>
@code
{
private Assembly[] Assemblies;
protected override void OnInitialized()
{
Assemblies = NavigationOptions.Value.Assemblies.ToArray();
}
}

View File

@@ -0,0 +1,8 @@
@using ShadcnBlazor.Sidebars
<header
class="flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)"
style="--header-height: calc(var(--spacing) * 12)">
<div class="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<SidebarTrigger ClassName="-ml-1"/>
</div>
</header>

View File

@@ -0,0 +1,126 @@
@inject NavigationManager Navigation
@inject IAuthorizationService AuthorizationService
@inject FrontendService FrontendService
@inject IEnumerable<ISidebarProvider> Providers
@using System.Text.Json
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Moonlight.Frontend.Infrastructure.Hooks
@using Moonlight.Frontend.Infrastructure.Models
@using Moonlight.Frontend.Shared.Frontend
@using ShadcnBlazor.Sidebars
@implements IDisposable
@{
var url = new Uri(Navigation.Uri);
}
<Sidebar Variant="SidebarVariant.Sidebar" Collapsible="SidebarCollapsible.Icon">
<SidebarHeader>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton>
<a href="/" class="flex flex-row items-center">
<img alt="Logo" src="/_content/Moonlight.Frontend/logo.svg" class="size-6"/>
<span class="ms-2.5 text-lg font-semibold">@FrontendConfiguration?.Name</span>
</a>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarHeader>
<SidebarContent>
@foreach (var group in Items.GroupBy(x => x.Group))
{
<SidebarGroup>
@if (!string.IsNullOrWhiteSpace(group.Key))
{
<SidebarGroupLabel>
@group.Key
</SidebarGroupLabel>
}
<SidebarGroupContent>
<SidebarMenu>
@foreach (var item in group.OrderBy(x => x.Order))
{
var isActive = item.IsExactPath
? item.Path == url.LocalPath
: url.LocalPath.StartsWith(item.Path, StringComparison.OrdinalIgnoreCase);
<SidebarMenuItem @key="item">
<SidebarMenuButton IsActive="@isActive">
<Slot>
<a href="@item.Path" @attributes="context">
<DynamicComponent Type="item.IconType"/>
<span>@item.Name</span>
</a>
</Slot>
</SidebarMenuButton>
</SidebarMenuItem>
}
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
}
</SidebarContent>
<SidebarFooter>
<NavUser/>
</SidebarFooter>
</Sidebar>
@code
{
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private readonly List<SidebarItem> Items = new();
private FrontendConfiguration? FrontendConfiguration;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
foreach (var provider in Providers)
{
var items = await provider.GetItemsAsync();
foreach (var item in items)
{
if (!string.IsNullOrWhiteSpace(item.Policy))
{
var result = await AuthorizationService.AuthorizeAsync(authState.User, item.Policy);
if (!result.Succeeded)
continue;
}
Items.Add(item);
}
}
Navigation.LocationChanged += OnLocationChanged;
FrontendConfiguration = await FrontendService.GetConfigurationAsync();
Console.WriteLine(JsonSerializer.Serialize(FrontendConfiguration));
}
private async void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
try
{
await InvokeAsync(StateHasChanged);
}
catch (Exception)
{
// Ignored
}
}
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
}
}

View File

@@ -0,0 +1,33 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Infrastructure.Configuration
@using Moonlight.Frontend.Infrastructure.Hooks
@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

@@ -0,0 +1,52 @@
@using Microsoft.Extensions.Options
@using Moonlight.Frontend.Infrastructure.Configuration
@using ShadcnBlazor.Extras.Alerts
@using ShadcnBlazor.Sidebars
@inherits LayoutComponentBase
@inject IOptions<LayoutPageOptions> LayoutPageOptions
<SidebarProvider DefaultOpen="true">
<AppSidebar/>
<SidebarInset>
<AppHeader/>
@foreach (var headerComponent in HeaderComponents)
{
<DynamicComponent Type="headerComponent"/>
}
<div class="mx-8 my-8 max-w-full">
<AlertLauncher/>
@Body
</div>
@foreach (var footerComponent in FooterComponents)
{
<DynamicComponent Type="footerComponent"/>
}
</SidebarInset>
</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,78 @@
@using System.Security.Claims
@using LucideBlazor
@using Microsoft.AspNetCore.Components.Authorization
@using ShadcnBlazor.Avatars
@using ShadcnBlazor.Dropdowns
@using ShadcnBlazor.Interop
@using ShadcnBlazor.Sidebars
@inject NavigationManager Navigation
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger>
<Slot>
<SidebarMenuButton
Size="SidebarMenuButtonSize.Lg"
ClassName="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
@attributes="context">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src="https://github.com/ghost.png" alt="Ghost"/>
<AvatarFallback className="rounded-lg">GH</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">@Username</span>
<span class="truncate text-xs">@Email</span>
</div>
<ChevronsUpDownIcon ClassName="ml-auto size-4"/>
</SidebarMenuButton>
</Slot>
</DropdownMenuTrigger>
<DropdownMenuContent
ClassName="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
Side="@(SidebarProvider.IsMobile ? PositionSide.Bottom : PositionSide.Right)"
Align="PositionAlignment.End">
<DropdownMenuLabel ClassName="p-0 font-normal">
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar ClassName="h-8 w-8 rounded-lg">
<AvatarImage src="https://github.com/ghost.png" alt="Ghost"/>
<AvatarFallback className="rounded-lg">GH</AvatarFallback>
</Avatar>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">@Username</span>
<span class="truncate text-xs">@Email</span>
</div>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator/>
<DropdownMenuItem OnClick="Logout">
<LogOutIcon/>
Log out
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
@code
{
[CascadingParameter] public SidebarProvider SidebarProvider { get; set; }
[CascadingParameter] public Task<AuthenticationState> AuthState { get; set; }
private string Username;
private string Email;
protected override async Task OnInitializedAsync()
{
var authState = await AuthState;
Username = authState.User.FindFirst(ClaimTypes.Name)?.Value ?? "N/A";
Email = authState.User.FindFirst(ClaimTypes.Email)?.Value ?? "N/A";
}
private void Logout()
{
Navigation.NavigateTo("/api/auth/logout", true);
}
}

View File

@@ -0,0 +1,23 @@
@page "/notfound"
@using LucideBlazor
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Emptys
<Empty>
<EmptyHeader>
<EmptyMedia Variant="EmptyMediaVariant.Icon">
<FolderOpenIcon/>
</EmptyMedia>
<EmptyTitle>Page not found</EmptyTitle>
<EmptyDescription>
The page you requested could not be found
</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button>
<Slot>
<a href="/" @attributes="context">Go to home</a>
</Slot>
</Button>
</EmptyContent>
</Empty>