Merge pull request #345 from Moonlight-Panel/v2_AddThemeFeature

Added theme feature
This commit is contained in:
Marcel Baumgartner 2023-12-22 23:03:29 +01:00 committed by GitHub
commit 0d8cc5bd5d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 1568 additions and 26 deletions

View file

@ -16,6 +16,12 @@ public class ConfigV1
[JsonProperty("Store")] public StoreData Store { get; set; } = new();
[JsonProperty("Theme")] public ThemeData Theme { get; set; } = new();
public class ThemeData
{
[JsonProperty("EnableDefault")] public bool EnableDefault { get; set; } = true;
}
public class StoreData
{
[JsonProperty("Currency")]

View file

@ -34,6 +34,9 @@ public class DataContext : DbContext
// Tickets
public DbSet<Ticket> Tickets { get; set; }
public DbSet<TicketMessage> TicketMessages { get; set; }
// Themes
public DbSet<Theme> Themes { get; set; }
public DataContext(ConfigService configService)
{

View file

@ -0,0 +1,13 @@
namespace Moonlight.App.Database.Entities;
public class Theme
{
public int Id { get; set; }
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; } = "";
public bool Enabled { get; set; } = false;
}

View file

@ -0,0 +1,697 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Moonlight.App.Database;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
[DbContext(typeof(DataContext))]
[Migration("20231222100225_AddThemeModel")]
partial class AddThemeModel
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.2");
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuthorId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.ToTable("Posts");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("AuthorId")
.HasColumnType("INTEGER");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int?>("PostId")
.HasColumnType("INTEGER");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AuthorId");
b.HasIndex("PostId");
b.ToTable("PostComments");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int?>("PostId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("PostId");
b.HasIndex("UserId");
b.ToTable("PostLikes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.WordFilter", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Filter")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("WordFilters");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Categories");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Percent")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("Coupons");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CouponId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CouponId");
b.HasIndex("UserId");
b.ToTable("CouponUses");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Amount")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Value")
.HasColumnType("REAL");
b.HasKey("Id");
b.ToTable("GiftCodes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("GiftCodeId")
.HasColumnType("INTEGER");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("GiftCodeId");
b.HasIndex("UserId");
b.ToTable("GiftCodeUses");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("CategoryId")
.HasColumnType("INTEGER");
b.Property<string>("ConfigJson")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Duration")
.HasColumnType("INTEGER");
b.Property<int>("MaxPerUser")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Slug")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Stock")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("CategoryId");
b.ToTable("Products");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("ConfigJsonOverride")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Nickname")
.HasColumnType("TEXT");
b.Property<int>("OwnerId")
.HasColumnType("INTEGER");
b.Property<int>("ProductId")
.HasColumnType("INTEGER");
b.Property<DateTime>("RenewAt")
.HasColumnType("TEXT");
b.Property<bool>("Suspended")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OwnerId");
b.HasIndex("ProductId");
b.ToTable("Services");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<int>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.HasIndex("UserId");
b.ToTable("ServiceShares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<double>("Price")
.HasColumnType("REAL");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("Transaction");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CssUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DonateUrl")
.HasColumnType("TEXT");
b.Property<bool>("Enabled")
.HasColumnType("INTEGER");
b.Property<string>("JsUrl")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Themes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<int>("CreatorId")
.HasColumnType("INTEGER");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("Open")
.HasColumnType("INTEGER");
b.Property<int>("Priority")
.HasColumnType("INTEGER");
b.Property<int?>("ServiceId")
.HasColumnType("INTEGER");
b.Property<string>("Tries")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatorId");
b.HasIndex("ServiceId");
b.ToTable("Tickets");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Attachment")
.HasColumnType("TEXT");
b.Property<string>("Content")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<bool>("IsSupport")
.HasColumnType("INTEGER");
b.Property<int?>("SenderId")
.HasColumnType("INTEGER");
b.Property<int?>("TicketId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("SenderId");
b.HasIndex("TicketId");
b.ToTable("TicketMessages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Avatar")
.HasColumnType("TEXT");
b.Property<double>("Balance")
.HasColumnType("REAL");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Flags")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Permissions")
.HasColumnType("INTEGER");
b.Property<DateTime>("TokenValidTimestamp")
.HasColumnType("TEXT");
b.Property<string>("TotpKey")
.HasColumnType("TEXT");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Users");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Author")
.WithMany()
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Author");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Author")
.WithMany()
.HasForeignKey("AuthorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Community.Post", null)
.WithMany("Comments")
.HasForeignKey("PostId");
b.Navigation("Author");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostLike", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Community.Post", null)
.WithMany("Likes")
.HasForeignKey("PostId");
b.HasOne("Moonlight.App.Database.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Coupon", "Coupon")
.WithMany()
.HasForeignKey("CouponId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("CouponUses")
.HasForeignKey("UserId");
b.Navigation("Coupon");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.GiftCode", "GiftCode")
.WithMany()
.HasForeignKey("GiftCodeId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("GiftCodeUses")
.HasForeignKey("UserId");
b.Navigation("GiftCode");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Product", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Category", "Category")
.WithMany()
.HasForeignKey("CategoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Category");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Owner")
.WithMany()
.HasForeignKey("OwnerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Store.Product", "Product")
.WithMany()
.HasForeignKey("ProductId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Owner");
b.Navigation("Product");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.ServiceShare", b =>
{
b.HasOne("Moonlight.App.Database.Entities.Store.Service", null)
.WithMany("Shares")
.HasForeignKey("ServiceId");
b.HasOne("Moonlight.App.Database.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Transaction", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", null)
.WithMany("Transactions")
.HasForeignKey("UserId");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Creator")
.WithMany()
.HasForeignKey("CreatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Moonlight.App.Database.Entities.Store.Service", "Service")
.WithMany()
.HasForeignKey("ServiceId");
b.Navigation("Creator");
b.Navigation("Service");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.TicketMessage", b =>
{
b.HasOne("Moonlight.App.Database.Entities.User", "Sender")
.WithMany()
.HasForeignKey("SenderId");
b.HasOne("Moonlight.App.Database.Entities.Tickets.Ticket", null)
.WithMany("Messages")
.HasForeignKey("TicketId");
b.Navigation("Sender");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Community.Post", b =>
{
b.Navigation("Comments");
b.Navigation("Likes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b =>
{
b.Navigation("Shares");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
{
b.Navigation("Messages");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.User", b =>
{
b.Navigation("CouponUses");
b.Navigation("GiftCodeUses");
b.Navigation("Transactions");
});
#pragma warning restore 612, 618
}
}
}

View file

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Moonlight.App.Database.Migrations
{
/// <inheritdoc />
public partial class AddThemeModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Themes",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Name = table.Column<string>(type: "TEXT", nullable: false),
Author = table.Column<string>(type: "TEXT", nullable: false),
DonateUrl = table.Column<string>(type: "TEXT", nullable: true),
CssUrl = table.Column<string>(type: "TEXT", nullable: false),
JsUrl = table.Column<string>(type: "TEXT", nullable: true),
Enabled = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Themes", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Themes");
}
}
}

View file

@ -357,6 +357,38 @@ namespace Moonlight.App.Database.Migrations
b.ToTable("Transaction");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Theme", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Author")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("CssUrl")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("DonateUrl")
.HasColumnType("TEXT");
b.Property<bool>("Enabled")
.HasColumnType("INTEGER");
b.Property<string>("JsUrl")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Themes");
});
modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b =>
{
b.Property<int>("Id")

View file

@ -0,0 +1,66 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Moonlight.App.Helpers;
using Moonlight.App.Services.Sys;
namespace Moonlight.App.Http.Controllers.Api;
[ApiController]
[Route("api/assetproxy")]
public class AssetProxyController : Controller
{
private readonly MoonlightThemeService ThemeService;
public AssetProxyController(MoonlightThemeService themeService)
{
ThemeService = themeService;
}
[HttpGet("theme/{id}/js")]
public async Task<ActionResult> GetThemeJs(int id)
{
var enabledThemes = await ThemeService.GetEnabled();
var selectedTheme = enabledThemes.FirstOrDefault(x => x.Id == id);
if (selectedTheme == null)
return NotFound();
try
{
using var httpClient = new HttpClient();
var content = await httpClient.GetByteArrayAsync(selectedTheme.JsUrl);
return File(content, "text/javascript");
}
catch (Exception e)
{
Logger.Warn($"Error proxying js for theme {id}");
Logger.Warn(e);
return Problem();
}
}
[HttpGet("theme/{id}/css")]
public async Task<ActionResult> GetThemeCss(int id)
{
var enabledThemes = await ThemeService.GetEnabled();
var selectedTheme = enabledThemes.FirstOrDefault(x => x.Id == id);
if (selectedTheme == null)
return NotFound();
try
{
using var httpClient = new HttpClient();
var content = await httpClient.GetByteArrayAsync(selectedTheme.CssUrl);
return File(content, "text/css");
}
catch (Exception e)
{
Logger.Warn($"Error proxying css for theme {id}");
Logger.Warn(e);
return Problem();
}
}
}

View file

@ -0,0 +1,13 @@
namespace Moonlight.App.Models.Abstractions;
public class ApplicationTheme
{
public int Id { get; set; }
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; } = "";
public bool Enabled { get; set; } = false;
}

View file

@ -0,0 +1,23 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Admin.Sys.Themes;
public class AddThemeForm
{
[Required(ErrorMessage = "You need to specify a name for your theme")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "You need to specify an author for your theme")]
public string Author { get; set; } = "";
[Description("Enter a url to date for your theme here in order to show up when other people use this theme")]
public string? DonateUrl { get; set; } = "";
[Required(ErrorMessage = "You need to specify a style sheet url")]
[Description("A url to your stylesheet")]
public string CssUrl { get; set; } = "";
[Description("(Optional) A url to your javascript file")]
public string? JsUrl { get; set; } = null;
}

View file

@ -0,0 +1,26 @@
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
namespace Moonlight.App.Models.Forms.Admin.Sys.Themes;
public class EditThemeForm
{
[Required(ErrorMessage = "You need to specify a name for your theme")]
public string Name { get; set; } = "";
[Required(ErrorMessage = "You need to specify an author for your theme")]
public string Author { get; set; } = "";
[Description("Enter a url to date for your theme here in order to show up when other people use this theme")]
public string? DonateUrl { get; set; } = "";
[Required(ErrorMessage = "You need to specify a style sheet url")]
[Description("A url to your stylesheet")]
public string CssUrl { get; set; } = "";
[Description("(Optional) A url to your javascript file")]
public string? JsUrl { get; set; } = null;
[Description("Enable the theme for this instance")]
public bool Enabled { get; set; } = false;
}

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

@ -7,11 +7,15 @@ namespace Moonlight.App.Services.Sys;
public class MoonlightService // This service can be used to perform strictly panel specific actions
{
private readonly ConfigService ConfigService;
public WebApplication Application { get; set; } // Do NOT modify using a plugin
private readonly IServiceProvider ServiceProvider;
public MoonlightService(ConfigService configService)
public WebApplication Application { get; set; } // Do NOT modify using a plugin
public MoonlightThemeService Theme { get; set; }
public MoonlightService(ConfigService configService, IServiceProvider serviceProvider)
{
ConfigService = configService;
ServiceProvider = serviceProvider;
}
public async Task Restart()

View file

@ -0,0 +1,51 @@
using Mappy.Net;
using Moonlight.App.Database.Entities;
using Moonlight.App.Models.Abstractions;
using Moonlight.App.Repositories;
namespace Moonlight.App.Services.Sys;
public class MoonlightThemeService
{
private readonly IServiceProvider ServiceProvider;
private readonly ConfigService ConfigService;
public MoonlightThemeService(IServiceProvider serviceProvider, ConfigService configService)
{
ServiceProvider = serviceProvider;
ConfigService = configService;
}
public Task<ApplicationTheme[]> GetInstalled()
{
using var scope = ServiceProvider.CreateScope();
var themeRepo = scope.ServiceProvider.GetRequiredService<Repository<Theme>>();
var themes = new List<ApplicationTheme>();
themes.AddRange(themeRepo
.Get()
.ToArray()
.Select(x => Mapper.Map<ApplicationTheme>(x)));
if (ConfigService.Get().Theme.EnableDefault)
{
themes.Insert(0, new()
{
Id = 0,
Name = "Moonlight Default",
Author = "MasuOwO",
Enabled = true,
CssUrl = "/css/theme.css",
DonateUrl = "https://ko-fi.com/masuowo"
});
}
return Task.FromResult(themes.ToArray());
}
public async Task<ApplicationTheme[]> GetEnabled() =>
(await GetInstalled())
.Where(x => x.Enabled)
.ToArray();
}

View file

@ -1,8 +1,15 @@
@page "/"
@using Microsoft.AspNetCore.Components.Web
@using Moonlight.App.Services.Sys
@namespace Moonlight.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@inject MoonlightThemeService MoonlightThemeService
@{
var themes = await MoonlightThemeService.GetEnabled();
}
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
@ -10,17 +17,25 @@
<base href="~/"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Moonlight</title>
<link rel="shortcut icon" href="/img/logo.svg">
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered"/>
<link href="/css/theme.css" rel="stylesheet" type="text/css"/>
@foreach (var theme in themes)
{
var finalUrl = theme.CssUrl.StartsWith("/") ? theme.CssUrl : $"/api/assetproxy/theme/{theme.Id}/css";
<!-- Theme: @(theme.Name) by @(theme.Author) -->
<link href="@(finalUrl)" rel="stylesheet" type="text/css"/>
}
<link href="/css/utils.css" rel="stylesheet" type="text/css"/>
<link href="/css/blazor.css" rel="stylesheet" type="text/css"/>
<link href="/css/boxicons.min.css" rel="stylesheet" type="text/css"/>
<link href="/css/sweetalert2dark.css" rel="stylesheet" type="text/css"/>
<link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css">
<link href="/css/interfont.css" rel="stylesheet" type="text/css"/>
@* <link href="https://fonts.googleapis.com/css?family=Inter:300,400,500,600,700" rel="stylesheet" type="text/css"> *@
</head>
<body data-kt-app-header-fixed="true"
data-kt-app-header-fixed-mobile="true"
@ -40,6 +55,18 @@
<script src="/_content/BlazorTable/BlazorTable.min.js"></script>
<script src="/js/sweetalert2.js"></script>
<script src="/js/ckeditor.js"></script>
@foreach (var theme in themes)
{
if (!string.IsNullOrEmpty(theme.JsUrl))
{
var finalUrl = theme.JsUrl.StartsWith("/") ? theme.JsUrl : $"/api/assetproxy/theme/{theme.Id}/js";
<!-- Theme: @(theme.Name) by @(theme.Author) -->
<script src="@(finalUrl)"></script>
}
}
<script src="/_framework/blazor.server.js"></script>
</body>
</html>

View file

@ -19,7 +19,6 @@ using Moonlight.App.Services.Utils;
using Serilog;
var configService = new ConfigService();
var moonlightService = new MoonlightService(configService);
Directory.CreateDirectory(PathBuilder.Dir("storage"));
Directory.CreateDirectory(PathBuilder.Dir("storage", "logs"));
@ -57,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>();
@ -95,7 +95,8 @@ builder.Services.AddSingleton(configService);
builder.Services.AddSingleton<SessionService>();
builder.Services.AddSingleton<BucketService>();
builder.Services.AddSingleton<MailService>();
builder.Services.AddSingleton(moonlightService);
builder.Services.AddSingleton<MoonlightService>();
builder.Services.AddSingleton<MoonlightThemeService>();
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
@ -112,7 +113,6 @@ var config =
builder.Logging.AddConfiguration(config.Build());
var app = builder.Build();
moonlightService.Application = app;
app.UseStaticFiles();
app.UseRouting();
@ -124,8 +124,10 @@ app.MapControllers();
// Auto start background services
app.Services.GetRequiredService<AutoMailSendService>();
var serviceService = app.Services.GetRequiredService<ServiceDefinitionService>();
var moonlightService = app.Services.GetRequiredService<MoonlightService>();
moonlightService.Application = app;
var serviceService = app.Services.GetRequiredService<ServiceDefinitionService>();
serviceService.Register<DummyServiceDefinition>(ServiceType.Server);
await pluginService.RunPrePost(app);

View file

@ -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);

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

@ -11,6 +11,11 @@
<i class="bx bx-sm bx-cog me-2"></i> Settings
</a>
</li>
<li class="nav-item mt-2">
<a class="nav-link text-active-primary ms-0 me-10 py-5 @(Index == 2 ? "active" : "")" href="/admin/sys/themes">
<i class="bx bx-sm bx-palette me-2"></i> Themes
</a>
</li>
</ul>
</div>
</div>

View file

@ -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>

View file

@ -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>

View file

@ -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>
@ -40,7 +42,7 @@
{
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");
return Task.CompletedTask;
}
}

View file

@ -0,0 +1,120 @@
@page "/admin/sys/themes"
@using Moonlight.App.Extensions.Attributes
@using Moonlight.App.Models.Enums
@using Moonlight.App.Models.Forms.Admin.Sys.Themes
@using Moonlight.App.Repositories
@using BlazorTable
@using Mappy.Net
@using Microsoft.AspNetCore.Components.Forms
@using Moonlight.App.Models.Json.Theme
@using Newtonsoft.Json
@attribute [RequirePermission(Permission.AdminRoot)]
@inject ToastService ToastService
@inject Repository<Theme> ThemeRepository
@inject FileDownloadService DownloadService
<AdminSysNavigation Index="2"/>
<div class="mt-5">
<AutoCrud TItem="Theme"
TCreateForm="AddThemeForm"
TUpdateForm="EditThemeForm"
Title="Manage themes"
Load="LoadData">
<Toolbar>
<SmartCustomFileSelect @ref="ThemeFileSelect" OnFileSelected="ImportTheme">
<div class="btn btn-secondary">
<i class="bx bx-sm bx-upload me-3"></i>
Import theme
</div>
</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 async Task ExportTheme(Theme theme)
{
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();
}
}
}

280
Moonlight/wwwroot/css/interfont.css vendored Normal file
View file

@ -0,0 +1,280 @@
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url() format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url() format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url() format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
src: url() format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
src: url("/fonts/Inter.woff2") format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

BIN
Moonlight/wwwroot/fonts/Inter.woff2 vendored Normal file

Binary file not shown.

View file

@ -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)
{