Implemented theme exporting and importing

This commit is contained in:
Marcel Baumgartner 2023-12-22 20:47:00 +01:00
parent c5a3c0550c
commit c11ff632d2
12 changed files with 239 additions and 49 deletions

View file

@ -0,0 +1,10 @@
namespace Moonlight.App.Models.Json.Theme;
public class ThemeExport
{
public string Name { get; set; } = "";
public string Author { get; set; } = "";
public string? DonateUrl { get; set; } = "";
public string CssUrl { get; set; } = "";
public string? JsUrl { get; set; } = "";
}

View file

@ -0,0 +1,10 @@
namespace Moonlight.App.Models.Json.Theme;
public class ThemeImport
{
public string Name { get; set; } = "";
public string Author { get; set; } = "";
public string? DonateUrl { get; set; } = "";
public string CssUrl { get; set; } = "";
public string? JsUrl { get; set; } = "";
}

View file

@ -0,0 +1,34 @@
using System.Text;
using Microsoft.JSInterop;
namespace Moonlight.App.Services.Interop;
public class FileDownloadService
{
private readonly IJSRuntime JsRuntime;
public FileDownloadService(IJSRuntime jsRuntime)
{
JsRuntime = jsRuntime;
}
public async Task DownloadStream(string fileName, Stream stream)
{
using var streamRef = new DotNetStreamReference(stream);
await JsRuntime.InvokeVoidAsync("moonlight.utils.download", fileName, streamRef);
}
public async Task DownloadBytes(string fileName, byte[] bytes)
{
var ms = new MemoryStream(bytes);
await DownloadStream(fileName, ms);
ms.Close();
await ms.DisposeAsync();
}
public async Task DownloadString(string fileName, string content) =>
await DownloadBytes(fileName, Encoding.UTF8.GetBytes(content));
}

View file

@ -55,7 +55,7 @@
@foreach (var theme in themes) @foreach (var theme in themes)
{ {
if (theme.JsUrl != null) if (!string.IsNullOrEmpty(theme.JsUrl))
{ {
<!-- Theme: @(theme.Name) by @(theme.Author) --> <!-- Theme: @(theme.Name) by @(theme.Author) -->
<script src="@(theme.JsUrl)"></script> <script src="@(theme.JsUrl)"></script>

View file

@ -56,6 +56,7 @@ builder.Services.AddScoped<CookieService>();
builder.Services.AddScoped<ToastService>(); builder.Services.AddScoped<ToastService>();
builder.Services.AddScoped<ModalService>(); builder.Services.AddScoped<ModalService>();
builder.Services.AddScoped<AlertService>(); builder.Services.AddScoped<AlertService>();
builder.Services.AddScoped<FileDownloadService>();
// Services / Store // Services / Store
builder.Services.AddScoped<StoreService>(); builder.Services.AddScoped<StoreService>();

View file

@ -14,7 +14,8 @@
<div class="card-header"> <div class="card-header">
<h3 class="card-title">@(Title)</h3> <h3 class="card-title">@(Title)</h3>
<div class="card-toolbar"> <div class="card-toolbar">
<button @onclick="StartCreate" class="btn btn-icon btn-success"> @Toolbar
<button @onclick="StartCreate" class="btn btn-icon btn-success ms-3">
<i class="bx bx-sm bx-plus"></i> <i class="bx bx-sm bx-plus"></i>
</button> </button>
</div> </div>
@ -26,7 +27,7 @@
PageSize="50" PageSize="50"
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6" TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
TableHeadClass="fw-bold text-muted"> TableHeadClass="fw-bold text-muted">
@ChildContent @View
<Column TableItem="TItem" Field="IdExpression" Title="" Sortable="false" Filterable="false"> <Column TableItem="TItem" Field="IdExpression" Title="" Sortable="false" Filterable="false">
<Template> <Template>
<div class="text-end"> <div class="text-end">
@ -109,7 +110,10 @@
public Func<Repository<TItem>, TItem[]> Load { get; set; } public Func<Repository<TItem>, TItem[]> Load { get; set; }
[Parameter] [Parameter]
public RenderFragment ChildContent { get; set; } public RenderFragment View { get; set; }
[Parameter]
public RenderFragment Toolbar { get; set; }
[Parameter] [Parameter]
public Func<TItem, Task>? ValidateAdd { get; set; } public Func<TItem, Task>? ValidateAdd { get; set; }
@ -144,6 +148,8 @@
IdExpression = CreateExpression(); IdExpression = CreateExpression();
} }
public async Task Reload() => await LazyLoader.Reload();
private Task LoadItems(LazyLoader _) private Task LoadItems(LazyLoader _)
{ {
Items = Load.Invoke(ItemRepository); Items = Load.Invoke(ItemRepository);

View file

@ -0,0 +1,56 @@
@using Microsoft.AspNetCore.Components.Forms
@inject ToastService ToastService
<InputFile OnChange="OnFileChanged" type="file" id="fileUpload" hidden=""/>
<label for="fileUpload" class="">
@if (SelectedFile == null)
{
@ChildContent
}
</label>
@code
{
[Parameter]
public IBrowserFile? SelectedFile { get; set; }
[Parameter]
public int MaxFileSize { get; set; } = 1024 * 1024 * 5;
[Parameter]
public Func<IBrowserFile, Task>? OnFileSelected { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
private async Task OnFileChanged(InputFileChangeEventArgs arg)
{
if (arg.FileCount > 0)
{
if (arg.File.Size < MaxFileSize)
{
SelectedFile = arg.File;
await InvokeAsync(StateHasChanged);
if(OnFileSelected != null)
await OnFileSelected.Invoke(SelectedFile);
return;
}
await ToastService.Danger($"The uploaded file should not be bigger than {Formatter.FormatSize(MaxFileSize)}");
}
SelectedFile = null;
await InvokeAsync(StateHasChanged);
}
public async Task RemoveSelection()
{
SelectedFile = null;
await InvokeAsync(StateHasChanged);
}
}

View file

@ -22,8 +22,10 @@
TUpdateForm="EditWordFilter" TUpdateForm="EditWordFilter"
Title="Manage word filter" Title="Manage word filter"
Load="LoadData"> Load="LoadData">
<Column TableItem="WordFilter" Field="@(x => x.Id)" Title="Id" Sortable="false" Filterable="true"/> <View>
<Column TableItem="WordFilter" Field="@(x => x.Filter)" Title="Filter" Sortable="false" Filterable="true"/> <Column TableItem="WordFilter" Field="@(x => x.Id)" Title="Id" Sortable="false" Filterable="true"/>
<Column TableItem="WordFilter" Field="@(x => x.Filter)" Title="Filter" Sortable="false" Filterable="true"/>
</View>
</AutoCrud> </AutoCrud>
</div> </div>

View file

@ -21,11 +21,13 @@
Load="LoadData" Load="LoadData"
ValidateAdd="Validate" ValidateAdd="Validate"
ValidateUpdate="Validate"> ValidateUpdate="Validate">
<Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/> <View>
<Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/> <Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/> <Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/>
<Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/> <Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/>
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/> <Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/>
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
</View>
</AutoCrud> </AutoCrud>
</div> </div>

View file

@ -20,10 +20,12 @@
Title="Manage gift codes" Title="Manage gift codes"
Load="LoadData" Load="LoadData"
ValidateAdd="Validate"> ValidateAdd="Validate">
<Column TableItem="GiftCode" Field="@(x => x.Code)" Title="Code" Sortable="false" Filterable="true" /> <View>
<Column TableItem="GiftCode" Field="@(x => x.Amount)" Title="Amount" Sortable="true" Filterable="true" /> <Column TableItem="GiftCode" Field="@(x => x.Code)" Title="Code" Sortable="false" Filterable="true"/>
<Column TableItem="GiftCode" Field="@(x => x.Value)" Title="Value" Sortable="true" Filterable="true" /> <Column TableItem="GiftCode" Field="@(x => x.Amount)" Title="Amount" Sortable="true" Filterable="true"/>
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/> <Column TableItem="GiftCode" Field="@(x => x.Value)" Title="Value" Sortable="true" Filterable="true"/>
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
</View>
</AutoCrud> </AutoCrud>
</div> </div>
@ -40,7 +42,7 @@
{ {
if (GiftCodeRepository.Get().Any(x => x.Code == giftCode.Code && x.Id != giftCode.Id)) if (GiftCodeRepository.Get().Any(x => x.Code == giftCode.Code && x.Id != giftCode.Id))
throw new DisplayException("A gift code with that code does already exist"); throw new DisplayException("A gift code with that code does already exist");
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View file

@ -4,13 +4,17 @@
@using Moonlight.App.Models.Enums @using Moonlight.App.Models.Enums
@using Moonlight.App.Models.Forms.Admin.Sys.Themes @using Moonlight.App.Models.Forms.Admin.Sys.Themes
@using Moonlight.App.Repositories @using Moonlight.App.Repositories
@using Moonlight.App.Services.Sys
@using BlazorTable @using BlazorTable
@using Moonlight.App.Models.Abstractions @using Mappy.Net
@using Microsoft.AspNetCore.Components.Forms
@using Moonlight.App.Models.Json.Theme
@using Newtonsoft.Json
@attribute [RequirePermission(Permission.AdminRoot)] @attribute [RequirePermission(Permission.AdminRoot)]
@inject MoonlightThemeService MoonlightThemeService @inject ToastService ToastService
@inject Repository<Theme> ThemeRepository
@inject FileDownloadService DownloadService
<AdminSysNavigation Index="2"/> <AdminSysNavigation Index="2"/>
@ -20,47 +24,97 @@
TUpdateForm="EditThemeForm" TUpdateForm="EditThemeForm"
Title="Manage themes" Title="Manage themes"
Load="LoadData"> Load="LoadData">
<Column TableItem="Theme" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/> <Toolbar>
<Column TableItem="Theme" Title="Name" Field="@(x => x.Name)" Sortable="true" Filterable="true"/> <SmartCustomFileSelect @ref="ThemeFileSelect" OnFileSelected="ImportTheme">
<Column TableItem="Theme" Title="Author" Field="@(x => x.Author)" Sortable="true" Filterable="true"/> <div class="btn btn-secondary">
<Column TableItem="Theme" Field="@(x => x.Id)" Title="" Filterable="false" Sortable="false"> <i class="bx bx-sm bx-upload me-3"></i>
<Template> Import theme
@if (context.Enabled)
{
<span class="text-success">Enabled</span>
}
else
{
<span class="text-muted">Disabled</span>
}
</Template>
</Column>
<Column TableItem="Theme" Field="@(x => x.Id)" Title="" Filterable="false" Sortable="false">
<Template>
<div class="text-end">
<div class="btn-group">
@if (context.DonateUrl != null)
{
<a class="btn btn-sm btn-info" href="@(context.DonateUrl)" target="_blank">Donate</a>
}
<WButton OnClick="() => ExportTheme(context)" Text="Export" CssClasses="btn btn-sm btn-secondary"/>
</div>
</div> </div>
</Template> </SmartCustomFileSelect>
</Column> </Toolbar>
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/> <View>
<Column TableItem="Theme" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
<Column TableItem="Theme" Title="Name" Field="@(x => x.Name)" Sortable="true" Filterable="true"/>
<Column TableItem="Theme" Title="Author" Field="@(x => x.Author)" Sortable="true" Filterable="true"/>
<Column TableItem="Theme" Field="@(x => x.Id)" Title="" Filterable="false" Sortable="false">
<Template>
@if (context.Enabled)
{
<span class="text-success">Enabled</span>
}
else
{
<span class="text-muted">Disabled</span>
}
</Template>
</Column>
<Column TableItem="Theme" Field="@(x => x.Id)" Title="" Filterable="false" Sortable="false">
<Template>
<div class="text-end">
<div class="btn-group">
@if (!string.IsNullOrEmpty(context.DonateUrl))
{
<a class="btn btn-info" href="@(context.DonateUrl)" target="_blank">Donate</a>
}
<WButton OnClick="() => ExportTheme(context)" Text="Export" CssClasses="btn btn-secondary"/>
</div>
</div>
</Template>
</Column>
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
</View>
</AutoCrud> </AutoCrud>
</div> </div>
@code @code
{ {
private SmartCustomFileSelect ThemeFileSelect;
private AutoCrud<Theme, AddThemeForm, EditThemeForm> AutoCrud;
private Theme[] LoadData(Repository<Theme> repository) private Theme[] LoadData(Repository<Theme> repository)
{ {
return repository.Get().ToArray(); return repository.Get().ToArray();
} }
private Task ExportTheme(Theme theme) private async Task ExportTheme(Theme theme)
{ {
return Task.CompletedTask; var model = Mapper.Map<ThemeExport>(theme);
var json = JsonConvert.SerializeObject(model, Formatting.Indented);
await ToastService.Info("Starting image download");
await DownloadService.DownloadString($"{model.Name}.json", json);
}
private async Task ImportTheme(IBrowserFile file)
{
try
{
if (file.ContentType != "application/json")
throw new DisplayException("Unknown file type. Only .json is supported");
var stream = file.OpenReadStream();
var streamReader = new StreamReader(stream);
var text = await streamReader.ReadToEndAsync();
var theme = JsonConvert.DeserializeObject<ThemeImport>(text);
if (theme == null)
throw new DisplayException("Unable to parse theme json");
var themeDb = Mapper.Map<Theme>(theme);
ThemeRepository.Add(themeDb);
await ToastService.Success($"Successfully imported theme '{theme.Name}'");
await AutoCrud.Reload();
}
catch (DisplayException e)
{
await ToastService.Danger(e.Message);
}
finally
{
await ThemeFileSelect.RemoveSelection();
}
} }
} }

View file

@ -109,6 +109,19 @@ window.moonlight = {
return text; return text;
} }
}, },
utils: {
download: async function (fileName, contentStreamReference) {
const arrayBuffer = await contentStreamReference.arrayBuffer();
const blob = new Blob([arrayBuffer]);
const url = URL.createObjectURL(blob);
const anchorElement = document.createElement('a');
anchorElement.href = url;
anchorElement.download = fileName ?? '';
anchorElement.click();
anchorElement.remove();
URL.revokeObjectURL(url);
}
},
textEditor: { textEditor: {
create: function(id) create: function(id)
{ {