diff --git a/Moonlight/App/Configuration/ConfigV1.cs b/Moonlight/App/Configuration/ConfigV1.cs index 1cbd38e8..78181300 100644 --- a/Moonlight/App/Configuration/ConfigV1.cs +++ b/Moonlight/App/Configuration/ConfigV1.cs @@ -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")] diff --git a/Moonlight/App/Database/DataContext.cs b/Moonlight/App/Database/DataContext.cs index a0ea380e..de9bf50c 100644 --- a/Moonlight/App/Database/DataContext.cs +++ b/Moonlight/App/Database/DataContext.cs @@ -34,6 +34,9 @@ public class DataContext : DbContext // Tickets public DbSet Tickets { get; set; } public DbSet TicketMessages { get; set; } + + // Themes + public DbSet Themes { get; set; } public DataContext(ConfigService configService) { diff --git a/Moonlight/App/Database/Entities/Theme.cs b/Moonlight/App/Database/Entities/Theme.cs new file mode 100644 index 00000000..81a17b35 --- /dev/null +++ b/Moonlight/App/Database/Entities/Theme.cs @@ -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; +} \ No newline at end of file diff --git a/Moonlight/App/Database/Migrations/20231222100225_AddThemeModel.Designer.cs b/Moonlight/App/Database/Migrations/20231222100225_AddThemeModel.Designer.cs new file mode 100644 index 00000000..33208aae --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231222100225_AddThemeModel.Designer.cs @@ -0,0 +1,697 @@ +// +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 + { + /// + 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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.ToTable("Posts"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Community.PostComment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("PostId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Filter") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WordFilters"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Coupon", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Percent") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Coupons"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.CouponUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CouponId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("REAL"); + + b.HasKey("Id"); + + b.ToTable("GiftCodes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.GiftCodeUse", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("GiftCodeId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CategoryId") + .HasColumnType("INTEGER"); + + b.Property("ConfigJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Duration") + .HasColumnType("INTEGER"); + + b.Property("MaxPerUser") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Stock") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.ToTable("Products"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Store.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConfigJsonOverride") + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Nickname") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("ProductId") + .HasColumnType("INTEGER"); + + b.Property("RenewAt") + .HasColumnType("TEXT"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Price") + .HasColumnType("REAL"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CssUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("JsUrl") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatorId") + .HasColumnType("INTEGER"); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Open") + .HasColumnType("INTEGER"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ServiceId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Attachment") + .HasColumnType("TEXT"); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("IsSupport") + .HasColumnType("INTEGER"); + + b.Property("SenderId") + .HasColumnType("INTEGER"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Balance") + .HasColumnType("REAL"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Flags") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("INTEGER"); + + b.Property("TokenValidTimestamp") + .HasColumnType("TEXT"); + + b.Property("TotpKey") + .HasColumnType("TEXT"); + + b.Property("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 + } + } +} diff --git a/Moonlight/App/Database/Migrations/20231222100225_AddThemeModel.cs b/Moonlight/App/Database/Migrations/20231222100225_AddThemeModel.cs new file mode 100644 index 00000000..9aadbbbf --- /dev/null +++ b/Moonlight/App/Database/Migrations/20231222100225_AddThemeModel.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Moonlight.App.Database.Migrations +{ + /// + public partial class AddThemeModel : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Themes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + Author = table.Column(type: "TEXT", nullable: false), + DonateUrl = table.Column(type: "TEXT", nullable: true), + CssUrl = table.Column(type: "TEXT", nullable: false), + JsUrl = table.Column(type: "TEXT", nullable: true), + Enabled = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Themes", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Themes"); + } + } +} diff --git a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs index 6e5d8421..3ca1af2c 100644 --- a/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs +++ b/Moonlight/App/Database/Migrations/DataContextModelSnapshot.cs @@ -357,6 +357,38 @@ namespace Moonlight.App.Database.Migrations b.ToTable("Transaction"); }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Theme", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CssUrl") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DonateUrl") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("JsUrl") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Themes"); + }); + modelBuilder.Entity("Moonlight.App.Database.Entities.Tickets.Ticket", b => { b.Property("Id") diff --git a/Moonlight/App/Http/Controllers/Api/AssetProxyController.cs b/Moonlight/App/Http/Controllers/Api/AssetProxyController.cs new file mode 100644 index 00000000..a8650a3c --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/AssetProxyController.cs @@ -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 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 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(); + } + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Abstractions/ApplicationTheme.cs b/Moonlight/App/Models/Abstractions/ApplicationTheme.cs new file mode 100644 index 00000000..60a20f17 --- /dev/null +++ b/Moonlight/App/Models/Abstractions/ApplicationTheme.cs @@ -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; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Admin/Sys/Themes/AddThemeForm.cs b/Moonlight/App/Models/Forms/Admin/Sys/Themes/AddThemeForm.cs new file mode 100644 index 00000000..7f048aa8 --- /dev/null +++ b/Moonlight/App/Models/Forms/Admin/Sys/Themes/AddThemeForm.cs @@ -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; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/Admin/Sys/Themes/EditThemeForm.cs b/Moonlight/App/Models/Forms/Admin/Sys/Themes/EditThemeForm.cs new file mode 100644 index 00000000..ac846b49 --- /dev/null +++ b/Moonlight/App/Models/Forms/Admin/Sys/Themes/EditThemeForm.cs @@ -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; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Json/Theme/ThemeExport.cs b/Moonlight/App/Models/Json/Theme/ThemeExport.cs new file mode 100644 index 00000000..fee81d6b --- /dev/null +++ b/Moonlight/App/Models/Json/Theme/ThemeExport.cs @@ -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; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Json/Theme/ThemeImport.cs b/Moonlight/App/Models/Json/Theme/ThemeImport.cs new file mode 100644 index 00000000..370dbfc5 --- /dev/null +++ b/Moonlight/App/Models/Json/Theme/ThemeImport.cs @@ -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; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Services/Interop/FileDownloadService.cs b/Moonlight/App/Services/Interop/FileDownloadService.cs new file mode 100644 index 00000000..572417e7 --- /dev/null +++ b/Moonlight/App/Services/Interop/FileDownloadService.cs @@ -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)); +} \ No newline at end of file diff --git a/Moonlight/App/Services/Sys/MoonlightService.cs b/Moonlight/App/Services/Sys/MoonlightService.cs index 4e2db0f8..a29f4ea9 100644 --- a/Moonlight/App/Services/Sys/MoonlightService.cs +++ b/Moonlight/App/Services/Sys/MoonlightService.cs @@ -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() diff --git a/Moonlight/App/Services/Sys/MoonlightThemeService.cs b/Moonlight/App/Services/Sys/MoonlightThemeService.cs new file mode 100644 index 00000000..281e3f55 --- /dev/null +++ b/Moonlight/App/Services/Sys/MoonlightThemeService.cs @@ -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 GetInstalled() + { + using var scope = ServiceProvider.CreateScope(); + var themeRepo = scope.ServiceProvider.GetRequiredService>(); + + var themes = new List(); + + themes.AddRange(themeRepo + .Get() + .ToArray() + .Select(x => Mapper.Map(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 GetEnabled() => + (await GetInstalled()) + .Where(x => x.Enabled) + .ToArray(); +} \ No newline at end of file diff --git a/Moonlight/Pages/_Host.cshtml b/Moonlight/Pages/_Host.cshtml index fb6551df..d738977a 100644 --- a/Moonlight/Pages/_Host.cshtml +++ b/Moonlight/Pages/_Host.cshtml @@ -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(); +} + @@ -10,17 +17,25 @@ Moonlight - + - + - - + + @foreach (var theme in themes) + { + var finalUrl = theme.CssUrl.StartsWith("/") ? theme.CssUrl : $"/api/assetproxy/theme/{theme.Id}/css"; + + + + } + - + + @* *@ + +@foreach (var theme in themes) +{ + if (!string.IsNullOrEmpty(theme.JsUrl)) + { + var finalUrl = theme.JsUrl.StartsWith("/") ? theme.JsUrl : $"/api/assetproxy/theme/{theme.Id}/js"; + + + + } +} + \ No newline at end of file diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 62fe67a8..f5c1482f 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -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(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); // Services / Store builder.Services.AddScoped(); @@ -95,7 +95,8 @@ builder.Services.AddSingleton(configService); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddSingleton(moonlightService); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); 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(); -var serviceService = app.Services.GetRequiredService(); +var moonlightService = app.Services.GetRequiredService(); +moonlightService.Application = app; +var serviceService = app.Services.GetRequiredService(); serviceService.Register(ServiceType.Server); await pluginService.RunPrePost(app); diff --git a/Moonlight/Shared/Components/Forms/AutoCrud.razor b/Moonlight/Shared/Components/Forms/AutoCrud.razor index e6b5bcc3..dfad589f 100644 --- a/Moonlight/Shared/Components/Forms/AutoCrud.razor +++ b/Moonlight/Shared/Components/Forms/AutoCrud.razor @@ -14,7 +14,8 @@

@(Title)

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