Implemented theme exporting and importing
This commit is contained in:
parent
c5a3c0550c
commit
c11ff632d2
12 changed files with 239 additions and 49 deletions
10
Moonlight/App/Models/Json/Theme/ThemeExport.cs
Normal file
10
Moonlight/App/Models/Json/Theme/ThemeExport.cs
Normal 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; } = "";
|
||||||
|
}
|
10
Moonlight/App/Models/Json/Theme/ThemeImport.cs
Normal file
10
Moonlight/App/Models/Json/Theme/ThemeImport.cs
Normal 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; } = "";
|
||||||
|
}
|
34
Moonlight/App/Services/Interop/FileDownloadService.cs
Normal file
34
Moonlight/App/Services/Interop/FileDownloadService.cs
Normal 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));
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
13
Moonlight/wwwroot/js/moonlight.js
vendored
13
Moonlight/wwwroot/js/moonlight.js
vendored
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in a new issue