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)
|
||||
{
|
||||
if (theme.JsUrl != null)
|
||||
if (!string.IsNullOrEmpty(theme.JsUrl))
|
||||
{
|
||||
<!-- Theme: @(theme.Name) by @(theme.Author) -->
|
||||
<script src="@(theme.JsUrl)"></script>
|
||||
|
|
|
@ -56,6 +56,7 @@ builder.Services.AddScoped<CookieService>();
|
|||
builder.Services.AddScoped<ToastService>();
|
||||
builder.Services.AddScoped<ModalService>();
|
||||
builder.Services.AddScoped<AlertService>();
|
||||
builder.Services.AddScoped<FileDownloadService>();
|
||||
|
||||
// Services / Store
|
||||
builder.Services.AddScoped<StoreService>();
|
||||
|
|
|
@ -14,7 +14,8 @@
|
|||
<div class="card-header">
|
||||
<h3 class="card-title">@(Title)</h3>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -26,7 +27,7 @@
|
|||
PageSize="50"
|
||||
TableClass="table table-row-bordered table-row-gray-100 align-middle gs-0 gy-3 fs-6"
|
||||
TableHeadClass="fw-bold text-muted">
|
||||
@ChildContent
|
||||
@View
|
||||
<Column TableItem="TItem" Field="IdExpression" Title="" Sortable="false" Filterable="false">
|
||||
<Template>
|
||||
<div class="text-end">
|
||||
|
@ -109,7 +110,10 @@
|
|||
public Func<Repository<TItem>, TItem[]> Load { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment ChildContent { get; set; }
|
||||
public RenderFragment View { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public RenderFragment Toolbar { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Func<TItem, Task>? ValidateAdd { get; set; }
|
||||
|
@ -144,6 +148,8 @@
|
|||
IdExpression = CreateExpression();
|
||||
}
|
||||
|
||||
public async Task Reload() => await LazyLoader.Reload();
|
||||
|
||||
private Task LoadItems(LazyLoader _)
|
||||
{
|
||||
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"
|
||||
Title="Manage word filter"
|
||||
Load="LoadData">
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -21,11 +21,13 @@
|
|||
Load="LoadData"
|
||||
ValidateAdd="Validate"
|
||||
ValidateUpdate="Validate">
|
||||
<Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
|
||||
<View>
|
||||
<Column TableItem="Coupon" Title="Id" Field="@(x => x.Id)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Code" Field="@(x => x.Code)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Amount" Field="@(x => x.Amount)" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="Coupon" Title="Percent" Field="@(x => x.Percent)" Sortable="true" Filterable="true"/>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
|
||||
</View>
|
||||
</AutoCrud>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -20,10 +20,12 @@
|
|||
Title="Manage gift codes"
|
||||
Load="LoadData"
|
||||
ValidateAdd="Validate">
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Code)" Title="Code" Sortable="false" Filterable="true" />
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Amount)" Title="Amount" Sortable="true" Filterable="true" />
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Value)" Title="Value" Sortable="true" Filterable="true" />
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
|
||||
<View>
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Code)" Title="Code" Sortable="false" Filterable="true"/>
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Amount)" Title="Amount" Sortable="true" Filterable="true"/>
|
||||
<Column TableItem="GiftCode" Field="@(x => x.Value)" Title="Value" Sortable="true" Filterable="true"/>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
|
||||
</View>
|
||||
</AutoCrud>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,13 +4,17 @@
|
|||
@using Moonlight.App.Models.Enums
|
||||
@using Moonlight.App.Models.Forms.Admin.Sys.Themes
|
||||
@using Moonlight.App.Repositories
|
||||
@using Moonlight.App.Services.Sys
|
||||
@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)]
|
||||
|
||||
@inject MoonlightThemeService MoonlightThemeService
|
||||
@inject ToastService ToastService
|
||||
@inject Repository<Theme> ThemeRepository
|
||||
@inject FileDownloadService DownloadService
|
||||
|
||||
<AdminSysNavigation Index="2"/>
|
||||
|
||||
|
@ -20,47 +24,97 @@
|
|||
TUpdateForm="EditThemeForm"
|
||||
Title="Manage themes"
|
||||
Load="LoadData">
|
||||
<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 (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>
|
||||
<Toolbar>
|
||||
<SmartCustomFileSelect @ref="ThemeFileSelect" OnFileSelected="ImportTheme">
|
||||
<div class="btn btn-secondary">
|
||||
<i class="bx bx-sm bx-upload me-3"></i>
|
||||
Import theme
|
||||
</div>
|
||||
</Template>
|
||||
</Column>
|
||||
<Pager ShowPageNumber="true" ShowTotalCount="true" AlwaysShow="true"/>
|
||||
</SmartCustomFileSelect>
|
||||
</Toolbar>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private SmartCustomFileSelect ThemeFileSelect;
|
||||
private AutoCrud<Theme, AddThemeForm, EditThemeForm> AutoCrud;
|
||||
|
||||
private Theme[] LoadData(Repository<Theme> repository)
|
||||
{
|
||||
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;
|
||||
}
|
||||
},
|
||||
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: {
|
||||
create: function(id)
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue