mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-20 13:34:37 +01:00
UI: Theme extensions (#4398)
* Theme extensions Adds the ability to choose the themeing strategy: Extend one of the existing themes (light or dark) or go fully custom. The latter was the only option up to now, which isn't ideal: - One had to provide a full-blown theme file overriding all variables - Tedious, error prone and hard to maintain, because one has to keep track of updates This PR makes it so that one can choose light or dark as base theme and do modifications on top. Benefit: You can specify a limited set of variables and might get away with 5-20 lines of CSS. * Ensure custom theme is present * Update checkout test
This commit is contained in:
parent
18ba0148ae
commit
6972e8a3db
6 changed files with 179 additions and 50 deletions
|
@ -193,6 +193,8 @@ namespace BTCPayServer.Tests
|
|||
Driver.WaitForElement(By.Id("OnChainWithLnInvoiceFallback"));
|
||||
Driver.SetCheckbox(By.Id("OnChainWithLnInvoiceFallback"), bip21);
|
||||
Driver.FindElement(By.Id("Save")).Click();
|
||||
Assert.Contains("Store successfully updated", FindAlertMessage().Text);
|
||||
Assert.True(Driver.FindElement(By.Id("UseNewCheckout")).Selected);
|
||||
}
|
||||
|
||||
public Mnemonic GenerateWallet(string cryptoCode = "BTC", string seed = "", bool? importkeys = null, bool isHotWallet = false, ScriptPubKeyType format = ScriptPubKeyType.Segwit)
|
||||
|
|
|
@ -982,7 +982,10 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
[HttpPost("server/theme")]
|
||||
public async Task<IActionResult> Theme(ThemeSettings model, [FromForm] bool RemoveLogoFile)
|
||||
public async Task<IActionResult> Theme(
|
||||
ThemeSettings model,
|
||||
[FromForm] bool RemoveLogoFile,
|
||||
[FromForm] bool RemoveCustomThemeFile)
|
||||
{
|
||||
var settingsChanged = false;
|
||||
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
|
||||
|
@ -991,6 +994,40 @@ namespace BTCPayServer.Controllers
|
|||
if (userId is null)
|
||||
return NotFound();
|
||||
|
||||
if (model.CustomThemeFile != null)
|
||||
{
|
||||
if (model.CustomThemeFile.ContentType.Equals("text/css", StringComparison.InvariantCulture))
|
||||
{
|
||||
// delete existing file
|
||||
if (!string.IsNullOrEmpty(settings.CustomThemeFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
|
||||
}
|
||||
|
||||
// add new file
|
||||
try
|
||||
{
|
||||
var storedFile = await _fileService.AddFile(model.CustomThemeFile, userId);
|
||||
settings.CustomThemeFileId = storedFile.Id;
|
||||
settingsChanged = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeFile), $"Could not save theme file: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeFile), "The uploaded theme file needs to be a CSS file");
|
||||
}
|
||||
}
|
||||
else if (RemoveCustomThemeFile && !string.IsNullOrEmpty(settings.CustomThemeFileId))
|
||||
{
|
||||
await _fileService.RemoveFile(settings.CustomThemeFileId, userId);
|
||||
settings.CustomThemeFileId = null;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (model.LogoFile != null)
|
||||
{
|
||||
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
|
||||
|
@ -1010,12 +1047,12 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}");
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), $"Could not save logo: {e.Message}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
|
||||
ModelState.AddModelError(nameof(settings.LogoFile), "The uploaded logo file needs to be an image");
|
||||
}
|
||||
}
|
||||
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
|
||||
|
@ -1025,14 +1062,28 @@ namespace BTCPayServer.Controllers
|
|||
settingsChanged = true;
|
||||
}
|
||||
|
||||
if (model.CustomTheme && !Uri.IsWellFormedUriString(model.CssUri, UriKind.RelativeOrAbsolute))
|
||||
if (model.CustomTheme && !string.IsNullOrEmpty(model.CustomThemeCssUri) && !Uri.IsWellFormedUriString(model.CustomThemeCssUri, UriKind.RelativeOrAbsolute))
|
||||
{
|
||||
ModelState.AddModelError(nameof(model.CustomTheme), "Please provide a non-empty theme URI");
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeCssUri), "Please provide a non-empty theme URI");
|
||||
}
|
||||
else if (settings.CustomTheme != model.CustomTheme)
|
||||
|
||||
if (settings.CustomThemeExtension != model.CustomThemeExtension)
|
||||
{
|
||||
// Require a custom theme to be defined in that case
|
||||
if (string.IsNullOrEmpty(model.CustomThemeCssUri) && string.IsNullOrEmpty(settings.CustomThemeFileId))
|
||||
{
|
||||
ModelState.AddModelError(nameof(settings.CustomThemeFile), "Please provide a custom theme");
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.CustomThemeExtension = model.CustomThemeExtension;
|
||||
settingsChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (settings.CustomTheme != model.CustomTheme)
|
||||
{
|
||||
settings.CustomTheme = model.CustomTheme;
|
||||
settings.CustomThemeCssUri = model.CustomThemeCssUri;
|
||||
settingsChanged = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,35 +2,53 @@ using System.ComponentModel.DataAnnotations;
|
|||
using Microsoft.AspNetCore.Http;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace BTCPayServer.Services
|
||||
namespace BTCPayServer.Services;
|
||||
|
||||
public enum ThemeExtension
|
||||
{
|
||||
public class ThemeSettings
|
||||
[Display(Name = "Does not extend a BTCPay Server theme, fully custom")]
|
||||
Custom,
|
||||
[Display(Name = "Extends the BTCPay Server Light theme")]
|
||||
Light,
|
||||
[Display(Name = "Extends the BTCPay Server Dark theme")]
|
||||
Dark
|
||||
}
|
||||
|
||||
public class ThemeSettings
|
||||
{
|
||||
[Display(Name = "Use custom theme")]
|
||||
public bool CustomTheme { get; set; }
|
||||
|
||||
[Display(Name = "Custom Theme Extension Type")]
|
||||
public ThemeExtension CustomThemeExtension { get; set; }
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom Theme CSS URL")]
|
||||
public string CustomThemeCssUri { get; set; }
|
||||
|
||||
[Display(Name = "Custom Theme File")]
|
||||
[JsonIgnore]
|
||||
public IFormFile CustomThemeFile { get; set; }
|
||||
|
||||
public string CustomThemeFileId { get; set; }
|
||||
|
||||
[Display(Name = "Logo")]
|
||||
[JsonIgnore]
|
||||
public IFormFile LogoFile { get; set; }
|
||||
|
||||
public string LogoFileId { get; set; }
|
||||
|
||||
public bool FirstRun { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
[Display(Name = "Use custom theme")]
|
||||
public bool CustomTheme { get; set; }
|
||||
// no logs
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Populate)]
|
||||
[MaxLength(500)]
|
||||
[Display(Name = "Custom Theme CSS URL")]
|
||||
public string CustomThemeCssUri { get; set; }
|
||||
|
||||
public string CssUri
|
||||
{
|
||||
get => CustomTheme ? CustomThemeCssUri : "/main/themes/default.css";
|
||||
}
|
||||
|
||||
public bool FirstRun { get; set; }
|
||||
|
||||
[Display(Name = "Logo")]
|
||||
[JsonIgnore]
|
||||
public IFormFile LogoFile { get; set; }
|
||||
|
||||
public string LogoFileId { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
// no logs
|
||||
return string.Empty;
|
||||
}
|
||||
public string CssUri
|
||||
{
|
||||
get => CustomTheme ? CustomThemeCssUri : "/main/themes/default.css";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Services
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@using BTCPayServer.Abstractions.Contracts
|
||||
@inject ThemeSettings Theme
|
||||
@inject IFileService FileService
|
||||
|
||||
@if (Theme.CustomTheme)
|
||||
{
|
||||
@if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CssUri))
|
||||
{ // legacy customization with CSS URI - keep it for backwards-compatibility
|
||||
<link href="@Context.Request.GetRelativePathOrAbsolute(Theme.CssUri)" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
else if (Theme.CustomTheme && !string.IsNullOrEmpty(Theme.CustomThemeFileId))
|
||||
{ // new customization uses theme file id provided by upload
|
||||
@if (Theme.CustomThemeExtension != ThemeExtension.Custom)
|
||||
{ // needs to be added for light and dark, because dark extends light
|
||||
<link href="~/main/themes/default.css" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
@if (Theme.CustomThemeExtension == ThemeExtension.Dark)
|
||||
{
|
||||
<link href="~/main/themes/default-dark.css" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
<link href="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Theme.CustomThemeFileId))" rel="stylesheet" asp-append-version="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<link href="~/main/themes/default.css" asp-append-version="true" rel="stylesheet" />
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
@using BTCPayServer.Abstractions.Contracts
|
||||
@using BTCPayServer.Services
|
||||
@model BTCPayServer.Services.ThemeSettings
|
||||
@inject IFileService FileService
|
||||
@{
|
||||
ViewData.SetActivePage(ServerNavPages.Theme, "Theme");
|
||||
var canUpload = await FileService.IsAvailable();
|
||||
var themeExtension = ((ThemeExtension[])Enum.GetValues(typeof(ThemeExtension))).Select(t =>
|
||||
new SelectListItem(typeof(ThemeExtension).DisplayName(t.ToString()), t == ThemeExtension.Custom ? null : t.ToString()));
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
|
@ -15,22 +18,61 @@
|
|||
<div class="row">
|
||||
<div class="col-xl-8 col-xxl-constrain">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<p>Use the default Light or Dark Themes, or provide a CSS theme file below.</p>
|
||||
<p>Use the default Light or Dark Themes, or provide a custom CSS theme file below.</p>
|
||||
|
||||
<div class="form-group d-flex align-items-center">
|
||||
<input asp-for="CustomTheme" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#CustomThemeSettings" aria-expanded="@Model.CustomTheme" aria-controls="CustomThemeSettings" />
|
||||
<label asp-for="CustomTheme" class="form-label mb-0"></label>
|
||||
<div class="d-flex align-items-center mb-3">
|
||||
<input asp-for="CustomTheme" type="checkbox" class="btcpay-toggle me-3" data-bs-toggle="collapse" data-bs-target="#CustomThemeSettings" aria-expanded="@(Model.CustomTheme)" aria-controls="CustomThemeSettings" />
|
||||
<div>
|
||||
<label asp-for="CustomTheme" class="form-label"></label>
|
||||
<div class="text-muted">
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#1-custom-themes" target="_blank" rel="noreferrer noopener">Adjust the design</a>
|
||||
of your BTCPay Server instance to your needs.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse @(Model.CustomTheme ? "show" : "")" id="CustomThemeSettings">
|
||||
<div class="form-group">
|
||||
<label asp-for="CustomThemeCssUri" class="form-label"></label>
|
||||
<a href="https://docs.btcpayserver.org/Development/Theme/#1-custom-themes" target="_blank" rel="noreferrer noopener">
|
||||
<span class="fa fa-question-circle-o text-secondary" title="More information..."></span>
|
||||
</a>
|
||||
<input asp-for="CustomThemeCssUri" class="form-control" />
|
||||
<span asp-validation-for="CustomThemeCssUri" class="text-danger"></span>
|
||||
<span asp-validation-for="CustomTheme" class="text-danger"></span>
|
||||
</div>
|
||||
@if (!string.IsNullOrEmpty(Model.CustomThemeCssUri))
|
||||
{
|
||||
<div class="form-group mb-0 pt-2">
|
||||
<label asp-for="CustomThemeCssUri" class="form-label" data-required></label>
|
||||
<input asp-for="CustomThemeCssUri" class="form-control"/>
|
||||
<span asp-validation-for="CustomThemeCssUri" class="text-danger"></span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="form-group pt-2">
|
||||
<label asp-for="CustomThemeExtension" class="form-label" data-required></label>
|
||||
<select asp-for="CustomThemeExtension" asp-items="@themeExtension" class="form-select w-auto"></select>
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<label asp-for="CustomThemeFile" class="form-label" data-required></label>
|
||||
@if (!string.IsNullOrEmpty(Model.CustomThemeFileId))
|
||||
{
|
||||
<button type="submit" class="btn btn-link p-0 text-danger" name="RemoveCustomThemeFile" value="true">
|
||||
<span class="fa fa-times"></span> Remove
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
@if (canUpload)
|
||||
{
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<input asp-for="CustomThemeFile" type="file" class="form-control flex-grow">
|
||||
@if (!string.IsNullOrEmpty(Model.CustomThemeFileId))
|
||||
{
|
||||
<a href="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.CustomThemeFileId))" target="_blank" rel="noreferrer noopener" class="text-nowrap">Custom CSS</a>
|
||||
}
|
||||
</div>
|
||||
<span asp-validation-for="CustomThemeFile" class="text-danger"></span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<input asp-for="CustomThemeFile" type="file" class="form-control" disabled>
|
||||
<p class="form-text text-muted">In order to upload a theme file, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 class="mt-5 mb-3">Branding</h3>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
--btcpay-body-text: var(--btcpay-white);
|
||||
--btcpay-body-text-muted: var(--btcpay-neutral-600);
|
||||
--btcpay-body-text-rgb: 255, 255, 255;
|
||||
--btcpay-body-link-accent: var(--btcpay-primary-300);
|
||||
--btcpay-body-link-accent: var(--btcpay-primary-accent);
|
||||
--btcpay-form-bg: var(--btcpay-bg-dark);
|
||||
--btcpay-form-text: var(--btcpay-neutral-800);
|
||||
--btcpay-form-text-label: var(--btcpay-neutral-900);
|
||||
|
@ -27,6 +27,7 @@
|
|||
--btcpay-nav-link-active: var(--btcpay-white);
|
||||
--btcpay-footer-link-accent: var(--btcpay-neutral-800);
|
||||
--btcpay-pre-bg: var(--btcpay-bg-dark);
|
||||
--btcpay-primary-accent: var(--btcpay-primary-300);
|
||||
--btcpay-secondary: transparent;
|
||||
--btcpay-secondary-text-active: var(--btcpay-primary);
|
||||
--btcpay-secondary-rgb: 22, 27, 34;
|
||||
|
|
Loading…
Add table
Reference in a new issue