First iteration of the plugin template
Some checks failed
Nuget: publish / publish (push) Has been cancelled

This commit is contained in:
2026-02-20 22:54:01 +01:00
commit ed2b4aa5f4
36 changed files with 1382 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
{
"$schema": "http://json.schemastore.org/template",
"author": "Moonlight-Panel",
"classifications": [
"Moonlight"
],
"name": "PluginTemplate",
"description": "Template for a plugin for moonlight",
"precedence": "0",
"identity": "Moonlight.PluginTemplate",
"shortName": "moonlight-plugintemplate",
"tags": {
"language": "C#",
"type": "project"
},
"sourceName": "PluginTemplate",
"sources": [
{
"source": "./",
"target": "./",
"include": [
"**/*"
],
"exclude": [
"**/[Bb]in/**",
"**/[Oo]bj/**",
"**/.template.config/**",
"**/*.filelist",
"**/*.user",
"**/*.lock.json",
"**/bun.lock",
"**/node_modules/**",
"**/storage/**",
"**/appsettings.Development.json",
"**/appsettings.json",
"**/.idea/**",
"**/.idea/**",
"**/style.min.css"
]
}
],
"postActions": [
{
"actionId": "AC1156F7-BB77-4DB8-B28F-24EEBCCA1E5C",
"condition": "true",
"id": "instructions",
"manualInstructions": [
{
"text": "Template successfully applied. https://moonlightpa.nl/dev"
}
]
}
]
}

View File

@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Api" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="SimplePlugin" Version="1.0.2" />
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PluginTemplate.Api\PluginTemplate.Api.csproj" />
<ProjectReference Include="..\PluginTemplate.Frontend.Host\PluginTemplate.Frontend.Host.csproj" />
</ItemGroup>
<Import Project="Api.props"/>
</Project>

View File

@@ -0,0 +1,9 @@
using Moonlight.Api;
using SimplePlugin.Generated;
var plugins = PluginRegistry
.Modules
.OfType<MoonlightPlugin>()
.ToArray();
await StartupHandler.RunAsync(args, plugins);

View File

@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5031",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7240;http://localhost:5031",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,23 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Moonlight": {
"Database": {
"Host": "your-db.host",
"Username": "change_me",
"Password": "change_me",
"Database": "change_me"
},
"Oidc": {
"Authority": "http://localhost:8092",
"RequireHttpsMetadata": false,
"ClientId": "clientId",
"ClientSecret": "clientSecret"
}
}
}

View File

@@ -0,0 +1,6 @@
<Project>
<ItemGroup>
<!-- Put your plugin references here -->
<!-- E.g. <PackageReference Include="MoonlightServers.Frontend" Version="2.1.0" /> -->
</ItemGroup>
</Project>

View File

@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<OverrideHtmlAssetPlaceholders>true</OverrideHtmlAssetPlaceholders>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.1" PrivateAssets="all"/>
<PackageReference Include="SimplePlugin" Version="1.0.2" />
<PackageReference Include="SimplePlugin.Abstractions" Version="1.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\PluginTemplate.Frontend\PluginTemplate.Frontend.csproj" />
</ItemGroup>
<Import Project="Frontend.props"/>
</Project>

View File

@@ -0,0 +1,9 @@
using Moonlight.Frontend;
using SimplePlugin.Generated;
var plugins = PluginRegistry
.Modules
.OfType<MoonlightPlugin>()
.ToArray();
await StartupHandler.RunAsync(args, plugins);

View File

@@ -0,0 +1,25 @@
import fs from 'fs';
import selectorParser from 'postcss-selector-parser';
export default function extractTailwindClasses(opts = {}) {
const classSet = new Set();
return {
postcssPlugin: 'extract-tailwind-classes',
Rule(rule) {
selectorParser(selectors => {
selectors.walkClasses(node => {
classSet.add(node.value);
});
}).processSync(rule.selector);
},
OnceExit() {
const classArray = Array.from(classSet).sort();
fs.mkdirSync('../../../PluginTemplate.Frontend/Styles', { recursive: true });
fs.writeFileSync('../../../PluginTemplate.Frontend/Styles/PluginTemplate.Frontend.map', classArray.join('\n'));
console.log(`Extracted classes ${classArray.length}`);
}
};
}
extractTailwindClasses.postcss = true;

View File

@@ -0,0 +1,19 @@
{
"scripts": {
"dev": "npx postcss styles.css -o ../wwwroot/style.min.css --watch",
"dev-build": "npx postcss styles.css -o ../wwwroot/style.min.css",
"build": "cross-env EXTRACT_CLASSES=true npx postcss styles.css -o ../wwwroot/style.min.css"
},
"dependencies": {
"@tailwindcss/postcss": "^4.1.18",
"cssnano": "^7.1.2",
"postcss": "^8.5.6",
"postcss-cli": "^11.0.1",
"postcss-selector-parser": "^7.1.1",
"tailwindcss": "^4.1.18",
"tw-animate-css": "^1.4.0"
},
"devDependencies": {
"cross-env": "^10.1.0"
}
}

View File

@@ -0,0 +1,18 @@
import tailwindcss from '@tailwindcss/postcss';
import cssnano from 'cssnano';
import extractTailwindClasses from './extract-classes.mjs';
const plugins = [
tailwindcss,
cssnano({
preset: 'default'
})
];
if (process.env.EXTRACT_CLASSES === "true") {
plugins.push(extractTailwindClasses());
}
export default {
plugins
};

View File

@@ -0,0 +1,31 @@
@import "tailwindcss";
@import "tw-animate-css";
@import "../bin/ShadcnBlazor/scrollbar.css";
@import "../bin/ShadcnBlazor/default-theme.css";
@import "./theme.css";
@source "../bin/Moonlight.Frontend/*.map";
@source "../../../Moonlight.Api/**/*.razor";
@source "../../../Moonlight.Api/**/*.cs";
@source "../../../Moonlight.Api/**/*.html";
@source "../../../Moonlight.Frontend/**/*.razor";
@source "../../../Moonlight.Frontend/**/*.cs";
@source "../../../Moonlight.Frontend/**/*.html";
@custom-variant dark (&:is(.dark *));
#blazor-error-ui {
display: none;
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,132 @@
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.129 0.042 264.695);
--card: oklch(1 0 0);
--card-foreground: oklch(0.129 0.042 264.695);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.129 0.042 264.695);
--primary: oklch(0.208 0.042 265.755);
--primary-foreground: oklch(0.984 0.003 247.858);
--secondary: oklch(0.968 0.007 247.896);
--secondary-foreground: oklch(0.208 0.042 265.755);
--muted: oklch(0.968 0.007 247.896);
--muted-foreground: oklch(0.554 0.046 257.417);
--accent: oklch(0.968 0.007 247.896);
--accent-foreground: oklch(0.208 0.042 265.755);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.929 0.013 255.508);
--input: oklch(0.929 0.013 255.508);
--ring: oklch(0.704 0.04 256.788);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: var(--background);
--sidebar-foreground: var(--foreground);
--sidebar-primary: var(--primary);
--sidebar-primary-foreground: var(--primary-foreground);
--sidebar-accent: var(--accent);
--sidebar-accent-foreground: var(--accent-foreground);
--sidebar-border: var(--border);
--sidebar-ring: var(--ring);
--font-sans: Inter, sans-serif;
--font-serif: Georgia, serif;
}
.dark {
/* Deep blue-slate background with purple undertones */
--background: oklch(0.16 0.028 260);
--foreground: oklch(0.98 0.008 260);
/* Cards with slightly lighter blue-slate */
--card: oklch(0.21 0.032 260);
--card-foreground: oklch(0.98 0.008 260);
/* Popovers with medium depth */
--popover: oklch(0.24 0.035 260);
--popover-foreground: oklch(0.98 0.008 260);
/* Vibrant blue-purple primary */
--primary: oklch(0.62 0.18 270);
--primary-foreground: oklch(0.99 0.005 260);
/* Secondary with blue-slate tone */
--secondary: oklch(0.27 0.038 260);
--secondary-foreground: oklch(0.98 0.008 260);
/* Muted elements */
--muted: oklch(0.25 0.035 260);
--muted-foreground: oklch(0.66 0.025 260);
/* Accent with purple-blue blend */
--accent: oklch(0.36 0.065 268);
--accent-foreground: oklch(0.98 0.008 260);
/* Destructive red with good contrast */
--destructive: oklch(0.62 0.22 25);
--destructive-foreground: oklch(0.99 0.005 260);
/* Subtle borders and inputs */
--border: oklch(0.32 0.025 260);
--input: oklch(0.30 0.030 260);
--ring: oklch(0.62 0.18 270);
/* Chart colors with blue-purple harmony */
--chart-1: oklch(0.58 0.18 270);
--chart-2: oklch(0.62 0.16 245);
--chart-3: oklch(0.68 0.15 290);
--chart-4: oklch(0.60 0.20 260);
--chart-5: oklch(0.65 0.14 280);
/* Sidebar with slightly different depth */
--sidebar: oklch(0.18 0.030 260);
--sidebar-foreground: oklch(0.97 0.008 260);
--sidebar-primary: oklch(0.60 0.17 270);
--sidebar-primary-foreground: oklch(0.99 0.005 260);
--sidebar-accent: oklch(0.26 0.038 260);
--sidebar-accent-foreground: oklch(0.98 0.008 260);
--sidebar-border: oklch(0.30 0.025 260);
--sidebar-ring: oklch(0.58 0.17 270);
}
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

View File

@@ -0,0 +1,108 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Moonlight</title>
<base href="/" />
<link rel="preload" id="webassembly" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=fallback" />
<link rel="stylesheet" href="style.min.css" />
<script type="importmap"></script>
<script>
window.frontendConfig = {
STYLE_TAG_ID: 'theme-variables',
configuration: {},
applyTheme: function(cssContent) {
// Find or create the style tag
let styleTag = document.getElementById(this.STYLE_TAG_ID);
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.id = this.STYLE_TAG_ID;
document.head.appendChild(styleTag);
}
// Update the style tag content
styleTag.textContent = cssContent;
},
reloadConfiguration: function (){
try {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/frontend/config', false);
xhr.send(null);
if (xhr.status === 200) {
this.configuration = JSON.parse(xhr.responseText);
}
} catch (error) {
console.error('Failed to load initial theme:', error);
}
},
getConfiguration: function (){
return this.configuration;
},
reload: function () {
this.reloadConfiguration();
document.title = this.configuration.name;
this.applyTheme(this.configuration.themeCss);
}
};
window.frontendConfig.reload();
</script>
</head>
<body class="bg-background text-foreground">
<div id="app">
<div class="h-screen w-full flex items-center justify-center">
<div class="flex min-w-0 flex-1 flex-col items-center justify-center gap-3 rounded-lg border-dashed p-6 text-center text-balance md:p-12">
<div class="flex max-w-sm flex-col items-center gap-2 text-center">
<div class="flex shrink-0 items-center justify-center [&_svg]:pointer-events-none [&_svg]:shrink-0 bg-muted text-foreground size-10 rounded-lg [&_svg:not([class*='size-'])]:size-6">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="lucide lucide-zap-icon lucide-zap size-6">
<path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/>
</svg>
</div>
</div>
<div class="text-lg font-medium tracking-tight">
Loading application
</div>
<div class="flex w-full max-w-sm min-w-0 flex-col items-center gap-4 text-sm text-balance">
<div class="bg-primary/20 w-full relative h-2 overflow-hidden rounded-full">
<div class="bg-primary h-full w-[var(--blazor-load-percentage,0)] flex-1 transition-all">
</div>
</div>
</div>
</div>
</div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="." class="reload">Reload</a>
<span class="dismiss">🗙</span>
</div>
<script src="/_content/ShadcnBlazor/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/interop.js" defer></script>
<script src="/_content/ShadcnBlazor.Extras/codemirror-bundle.js" defer></script>
<script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script>
</body>
</html>

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PluginTemplate.Shared.Http.Requests;
using PluginTemplate.Shared.Http.Responses;
namespace PluginTemplate.Api.Http.Controllers;
[Authorize]
[ApiController]
[Route("api/form")]
public class FormController : Controller
{
[HttpPost]
public async Task<ActionResult<FormResultDto>> PostAsync([FromBody] FormSubmitDto dto)
{
return new FormResultDto()
{
Result = dto.TextField.Replace(" ", string.Empty)
};
}
}

View File

@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moonlight.Api" Version="2.1.0">
<ExcludeAssets>content;contentfiles</ExcludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<Folder Include="Configuration\" />
<Folder Include="Database\Entities\" />
<Folder Include="Database\Migrations\" />
<Folder Include="Helpers\" />
<Folder Include="Implementations\" />
<Folder Include="Interfaces\" />
<Folder Include="Mappers\" />
<Folder Include="Models\" />
<Folder Include="Services\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginTemplate.Shared\PluginTemplate.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Api;
using SimplePlugin.Abstractions;
using SerializationContext = PluginTemplate.Shared.SerializationContext;
namespace PluginTemplate.Api;
[PluginModule]
public class Startup : MoonlightPlugin
{
public override void PreBuild(WebApplicationBuilder builder)
{
builder.Services.AddControllers()
.AddApplicationPart(typeof(Startup).Assembly)
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.TypeInfoResolverChain.Add(SerializationContext.Default);
});
}
}

View File

@@ -0,0 +1,17 @@
using LucideBlazor;
using Moonlight.Frontend.Interfaces;
using Moonlight.Frontend.Models;
namespace PluginTemplate.Frontend.Implementations;
public class PermissionProvider : IPermissionProvider
{
public Task<PermissionCategory[]> GetPermissionsAsync()
{
return Task.FromResult<PermissionCategory[]>([
new PermissionCategory("Demo", typeof(SparklesIcon), [
new Permission("Permissions:Demo", "Demo", "Access to demo page")
])
]);
}
}

View File

@@ -0,0 +1,21 @@
using LucideBlazor;
using Moonlight.Frontend.Interfaces;
using Moonlight.Frontend.Models;
namespace PluginTemplate.Frontend.Implementations;
public sealed class SidebarProvider : ISidebarProvider
{
public Task<SidebarItem[]> GetItemsAsync()
{
return Task.FromResult<SidebarItem[]>([
new SidebarItem()
{
Group = "Demo",
Name = "Demo",
IconType = typeof(SparklesIcon),
Path = "/demo"
}
]);
}
}

View File

@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<SupportedPlatform Include="browser"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Moonlight.Frontend" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PluginTemplate.Shared\PluginTemplate.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<None Include="Styles/*" Pack="true" PackagePath="Styles/" />
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="build\PluginTemplate.Frontend.targets" />
<None Include="Moonlight.Frontend.targets" Pack="true" PackagePath="buildTransitive\PluginTemplate.Frontend.targets" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
<Project>
<PropertyGroup>
<PluginTemplateCssClassDir Condition="'$(PluginTemplateCssClassDir)' == ''">
$(MSBuildProjectDirectory)\bin\PluginTemplate
</PluginTemplateCssClassDir>
</PropertyGroup>
<Target Name="PluginTemplate_CopyContents" BeforeTargets="Build">
<ItemGroup>
<Files Include="$(MSBuildThisFileDirectory)..\Styles\**\*" />
</ItemGroup>
<Copy SourceFiles="@(Files)" DestinationFolder="$(PluginTemplateCssClassDir)" SkipUnchangedFiles="true" />
</Target>
</Project>

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Moonlight.Frontend;
using Moonlight.Frontend.Configuration;
using Moonlight.Frontend.Interfaces;
using PluginTemplate.Frontend.Implementations;
using SimplePlugin.Abstractions;
namespace PluginTemplate.Frontend;
[PluginModule]
public sealed class Startup : MoonlightPlugin
{
public override void PreBuild(WebAssemblyHostBuilder builder)
{
builder.Services.AddSingleton<IPermissionProvider, PermissionProvider>();
builder.Services.AddSingleton<ISidebarProvider, SidebarProvider>();
builder.Services.Configure<NavigationAssemblyOptions>(options =>
{
options.Assemblies.Add(typeof(Startup).Assembly);
});
}
public override void PostBuild(WebAssemblyHost application)
{
}
}

View File

@@ -0,0 +1,74 @@
@using Moonlight.Frontend.Helpers
@using PluginTemplate.Shared
@using PluginTemplate.Shared.Http.Requests
@using PluginTemplate.Shared.Http.Responses
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Dialogs
@using ShadcnBlazor.Extras.AlertDialogs
@using ShadcnBlazor.Extras.Forms
@using ShadcnBlazor.Fields
@using ShadcnBlazor.Inputs
@inherits ShadcnBlazor.Extras.Dialogs.DialogBase
@inject HttpClient HttpClient
@inject AlertDialogService AlertDialogService
<DialogHeader>
<DialogTitle>Example Form</DialogTitle>
<DialogDescription>This forms removes all spaces from the input</DialogDescription>
</DialogHeader>
<EnhancedEditForm @ref="Form" OnValidSubmit="OnSubmit" Model="Dto">
<DataAnnotationsValidator/>
<FieldSet>
<FormValidationSummary/>
<FieldGroup>
<Field>
<FieldLabel for="formInput">Form Input</FieldLabel>
<TextInputField id="formInput" @bind-Value="Dto.TextField"/>
<FieldDescription>Input you want to remove the spaces from</FieldDescription>
</Field>
</FieldGroup>
</FieldSet>
</EnhancedEditForm>
<DialogFooter>
<Button @onclick="() => Form.SubmitAsync()">Submit</Button>
<DialogClose/>
</DialogFooter>
@code
{
private FormSubmitDto Dto = new();
private EnhancedEditForm Form;
private async Task<bool> OnSubmit(EditContext editContext, ValidationMessageStore validationMessageStore)
{
var response = await HttpClient.PostAsJsonAsync(
"api/form",
Dto,
SerializationContext.Default.Options
);
if (response.IsSuccessStatusCode)
{
var data = await response.Content.ReadFromJsonAsync<FormResultDto>(
SerializationContext.Default.Options
);
if (data == null)
return true;
await AlertDialogService.InfoAsync("Result", data.Result);
await CloseAsync();
return true;
}
await ProblemDetailsHelper.HandleProblemDetailsAsync(response, Dto, validationMessageStore);
return false;
}
}

View File

@@ -0,0 +1,44 @@
@page "/demo"
@using LucideBlazor
@using PluginTemplate.Frontend.UI.Components
@using ShadcnBlazor.Buttons
@using ShadcnBlazor.Cards
@using ShadcnBlazor.Extras.Dialogs
@inject DialogService DialogService
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5"
>
<Card ClassName="col-span-1">
<CardHeader>
<CardTitle>Demo</CardTitle>
<CardDescription>A cool demo page</CardDescription>
</CardHeader>
<CardContent>
You successfully used the plugin template to create your moonlight plugin :)
</CardContent>
<CardFooter>
<Button>
<Slot>
<a @attributes="context" href="https://moonlightpa.nl/dev">
<ExternalLinkIcon />
Visit documentation
</a>
</Slot>
</Button>
</CardFooter>
</Card>
<Card>
<CardContent>
<Button @onclick="LaunchFormAsync" Variant="ButtonVariant.Outline">
Open Form
</Button>
</CardContent>
</Card>
</div>
@code
{
private async Task LaunchFormAsync()
=> await DialogService.LaunchAsync<FormDialog>();
}

View File

@@ -0,0 +1,6 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization

View File

@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
namespace PluginTemplate.Shared.Http.Requests;
public class FormSubmitDto
{
[Required]
[MaxLength(32)]
public string TextField { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace PluginTemplate.Shared.Http.Responses;
public class FormResultDto
{
public string Result { get; set; }
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,15 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using PluginTemplate.Shared.Http.Requests;
using PluginTemplate.Shared.Http.Responses;
namespace PluginTemplate.Shared;
[JsonSerializable(typeof(FormSubmitDto))]
[JsonSerializable(typeof(FormResultDto))]
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web)]
public partial class SerializationContext : JsonSerializerContext
{
}

View File

@@ -0,0 +1,9 @@
<Solution>
<Folder Name="/Hosts/">
<Project Path="Hosts/PluginTemplate.Api.Host/PluginTemplate.Api.Host.csproj" />
<Project Path="Hosts/PluginTemplate.Frontend.Host/PluginTemplate.Frontend.Host.csproj" />
</Folder>
<Project Path="PluginTemplate.Api/PluginTemplate.Api.csproj" />
<Project Path="PluginTemplate.Frontend/PluginTemplate.Frontend.csproj" />
<Project Path="PluginTemplate.Shared/PluginTemplate.Shared.csproj" />
</Solution>