Add option to customize the instance logo (#4258)

* Add option to customize the instance logo


Custom logo for BTCPay instances

* Incorporate SVGUse helper
This commit is contained in:
d11n 2022-11-14 14:29:23 +01:00 committed by GitHub
parent c8a1024e24
commit 17f3b4125b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 217 additions and 97 deletions

View file

@ -0,0 +1,20 @@
@using BTCPayServer.Services
@using BTCPayServer.Abstractions.Contracts
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.TagHelpers
@inject ThemeSettings Theme
@inject IFileService FileService
@model BTCPayServer.Components.MainLogo.MainLogoViewModel
@if (!string.IsNullOrEmpty(Theme.LogoFileId))
{
var logoSrc = await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Theme.LogoFileId);
<img src="@logoSrc" alt="BTCPay Server" class="main-logo main-logo-custom @Model.CssClass" />
}
else
{
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="main-logo main-logo-btcpay @Model.CssClass">
<use href="/img/logo.svg#small" class="main-logo-btcpay--small"/>
<use href="/img/logo.svg#large" class="main-logo-btcpay--large"/>
</svg>
}

View file

@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Mvc;
namespace BTCPayServer.Components.MainLogo
{
public class MainLogo : ViewComponent
{
public IViewComponentResult Invoke(string cssClass = null)
{
var vm = new MainLogoViewModel
{
CssClass = cssClass,
};
return View(vm);
}
}
}

View file

@ -0,0 +1,7 @@
namespace BTCPayServer.Components.MainLogo
{
public class MainLogoViewModel
{
public string CssClass { get; set; }
}
}

View file

@ -1,9 +1,9 @@
@using Microsoft.AspNetCore.Mvc.TagHelpers
@using BTCPayServer.Abstractions.Extensions
@using BTCPayServer.Abstractions.TagHelpers
@using BTCPayServer.Abstractions.Contracts
@inject BTCPayServer.Services.BTCPayServerEnvironment Env
@using BTCPayServer.Services
@inject SignInManager<ApplicationUser> SignInManager
@inject BTCPayServerEnvironment Env
@inject IFileService FileService
@model BTCPayServer.Components.StoreSelector.StoreSelectorViewModel
@functions {
@ -11,14 +11,14 @@
#pragma warning disable 1998
private async Task LogoContent()
{
var logoSrc = $"{ViewContext.HttpContext.Request.PathBase}/img/logo.svg";
<svg xmlns="http://www.w3.org/2000/svg" role="img" alt="BTCPay Server" class="logo"><use href="@logoSrc#small" class="logo-small" /><use href="@logoSrc#large" class="logo-large" /></svg>
<vc:main-logo />
@if (Env.NetworkType != NBitcoin.ChainName.Mainnet)
{
<span class="badge bg-warning ms-1 ms-sm-0" style="font-size:10px;">@Env.NetworkType.ToString()</span>
var type = Env.NetworkType.ToString();
<small class="badge bg-warning rounded-pill ms-1 ms-sm-0" title="@type">@type.Replace("Testnet", "TN").Replace("Regtest", "RT")</small>
}
}
private string StoreName(string title)
private static string StoreName(string title)
{
return string.IsNullOrEmpty(title) ? "Unnamed Store" : title;
}
@ -26,15 +26,15 @@
}
@if (Model.CurrentStoreId == null)
{
<a href="~/" class="navbar-brand py-2 js-scroll-trigger">@{await LogoContent();}</a>
<a asp-controller="UIHome" asp-action="Index" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
else if (Model.CurrentStoreIsOwner)
{
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" class="navbar-brand py-2 js-scroll-trigger">@{await LogoContent();}</a>
<a asp-controller="UIStores" asp-action="Dashboard" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
else
{
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" class="navbar-brand py-2 js-scroll-trigger">@{await LogoContent();}</a>
<a asp-controller="UIInvoice" asp-action="ListInvoices" asp-route-storeId="@Model.CurrentStoreId" id="StoreSelectorHome" class="navbar-brand py-2">@{await LogoContent();}</a>
}
<div id="StoreSelector">

View file

@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers
Dictionary<string, string> directUrlByFiles = new Dictionary<string, string>();
foreach (string filename in fileIds)
{
string fileUrl = await _FileService.GetFileUrl(Request.GetAbsoluteRootUri(), filename);
string fileUrl = await _fileService.GetFileUrl(Request.GetAbsoluteRootUri(), filename);
if (fileUrl == null)
{
allFilesExist = false;
@ -71,7 +71,7 @@ namespace BTCPayServer.Controllers
{
try
{
await _FileService.RemoveFile(fileId, null);
await _fileService.RemoveFile(fileId, null);
return RedirectToAction(nameof(Files), new
{
fileIds = Array.Empty<string>(),
@ -142,7 +142,7 @@ namespace BTCPayServer.Controllers
throw new ArgumentOutOfRangeException();
}
var url = await _FileService.GetTemporaryFileUrl(Request.GetAbsoluteRootUri(), fileId, expiry, viewModel.IsDownload);
var url = await _fileService.GetTemporaryFileUrl(Request.GetAbsoluteRootUri(), fileId, expiry, viewModel.IsDownload);
TempData.SetStatusMessageModel(new StatusMessageModel()
{
Severity = StatusMessageModel.StatusSeverity.Success,
@ -183,7 +183,7 @@ namespace BTCPayServer.Controllers
invalidFileNameCount++;
continue;
}
var newFile = await _FileService.AddFile(file, GetUserId());
var newFile = await _fileService.AddFile(file, GetUserId());
fileIds.Add(newFile.Id);
}

View file

@ -10,6 +10,7 @@ using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using BTCPayServer.Abstractions.Constants;
using BTCPayServer.Abstractions.Contracts;
using BTCPayServer.Abstractions.Extensions;
using BTCPayServer.Abstractions.Models;
using BTCPayServer.Configuration;
@ -61,7 +62,7 @@ namespace BTCPayServer.Controllers
private readonly IOptions<ExternalServicesOptions> _externalServiceOptions;
private readonly Logs Logs;
private readonly StoredFileRepository _StoredFileRepository;
private readonly FileService _FileService;
private readonly IFileService _fileService;
private readonly IEnumerable<IStorageProviderService> _StorageProviderServices;
private readonly LinkGenerator _linkGenerator;
private readonly EmailSenderFactory _emailSenderFactory;
@ -70,7 +71,7 @@ namespace BTCPayServer.Controllers
UserManager<ApplicationUser> userManager,
UserService userService,
StoredFileRepository storedFileRepository,
FileService fileService,
IFileService fileService,
IEnumerable<IStorageProviderService> storageProviderServices,
BTCPayServerOptions options,
SettingsRepository settingsRepository,
@ -92,7 +93,7 @@ namespace BTCPayServer.Controllers
_policiesSettings = policiesSettings;
_Options = options;
_StoredFileRepository = storedFileRepository;
_FileService = fileService;
_fileService = fileService;
_StorageProviderServices = storageProviderServices;
_UserManager = userManager;
_userService = userService;
@ -972,22 +973,69 @@ namespace BTCPayServer.Controllers
return RedirectToAction(nameof(Services));
}
[Route("server/theme")]
[HttpGet("server/theme")]
public async Task<IActionResult> Theme()
{
var data = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
return View(data);
}
[Route("server/theme")]
[HttpPost]
public async Task<IActionResult> Theme(ThemeSettings settings)
[HttpPost("server/theme")]
public async Task<IActionResult> Theme(ThemeSettings model, [FromForm] bool RemoveLogoFile)
{
if (settings.CustomTheme && !Uri.IsWellFormedUriString(settings.CssUri, UriKind.RelativeOrAbsolute))
var settingsChanged = false;
var settings = await _SettingsRepository.GetSettingAsync<ThemeSettings>() ?? new ThemeSettings();
var userId = GetUserId();
if (userId is null)
return NotFound();
if (model.LogoFile != null)
{
TempData[WellKnownTempData.ErrorMessage] = "Please provide a non-empty theme URI";
if (model.LogoFile.ContentType.StartsWith("image/", StringComparison.InvariantCulture))
{
// delete existing image
if (!string.IsNullOrEmpty(settings.LogoFileId))
{
await _fileService.RemoveFile(settings.LogoFileId, userId);
}
// add new image
try
{
var storedFile = await _fileService.AddFile(model.LogoFile, userId);
settings.LogoFileId = storedFile.Id;
settingsChanged = true;
}
catch (Exception e)
{
ModelState.AddModelError(nameof(model.LogoFile), $"Could not save logo: {e.Message}");
}
}
else
{
ModelState.AddModelError(nameof(model.LogoFile), "The uploaded logo file needs to be an image");
}
}
else
else if (RemoveLogoFile && !string.IsNullOrEmpty(settings.LogoFileId))
{
await _fileService.RemoveFile(settings.LogoFileId, userId);
settings.LogoFileId = null;
settingsChanged = true;
}
if (model.CustomTheme && !Uri.IsWellFormedUriString(model.CssUri, UriKind.RelativeOrAbsolute))
{
ModelState.AddModelError(nameof(model.CustomTheme), "Please provide a non-empty theme URI");
}
else if (settings.CustomTheme != model.CustomTheme)
{
settings.CustomTheme = model.CustomTheme;
settings.CustomThemeCssUri = model.CustomThemeCssUri;
settingsChanged = true;
}
if (settingsChanged)
{
await _SettingsRepository.UpdateSetting(settings);
TempData[WellKnownTempData.SuccessMessage] = "Theme settings updated successfully";
@ -996,7 +1044,6 @@ namespace BTCPayServer.Controllers
return View(settings);
}
[Route("server/emails")]
public async Task<IActionResult> Emails()
{

View file

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
namespace BTCPayServer.Services
@ -19,6 +20,13 @@ namespace BTCPayServer.Services
}
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

View file

@ -266,7 +266,7 @@
<div class="me-3" v-text="`Updated ${lastUpdated}`">Updated @Model.Info.LastUpdated</div>
@if (!Theme.CustomTheme)
{
<vc:theme-switch css-class="text-muted me-3" responsive="none"/>
<vc:theme-switch css-class="text-muted me-3" />
}
<div class="form-check me-3 my-0 only-for-js" v-if="srvModel.animationsEnabled || animation">
<input class="form-check-input" type="checkbox" id="cbAnime" v-model="animation">

View file

@ -13,10 +13,12 @@
background: var(--btcpay-bg-tile);
border-radius: var(--btcpay-border-radius);
}
.account-form h4 {
margin-bottom: 1.5rem;
}
.main-logo { height: 4rem; width: 18rem; }
.main-logo.main-logo-btcpay { height: 4.5rem; width: 2.5rem; }
.main-logo-btcpay .main-logo-btcpay--large { display: none; }
</style>
@await RenderSectionAsync("PageHeadContent", false)
}
@ -28,8 +30,8 @@
<div class="row justify-content-center mb-2">
<div class="col text-center">
<a asp-controller="UIHome" asp-action="Index" tabindex="-1">
<img src="~/img/btcpay-logo.svg" alt="BTCPay Server" class="mb-4" height="70" asp-append-version="true"/>
<a asp-controller="UIHome" asp-action="Index" tabindex="-1" class="d-inline-block navbar-brand mx-0 mb-4">
<vc:main-logo />
</a>
<h1 class="h2 mb-3">Welcome to your BTCPay&nbsp;Server</h1>

View file

@ -1,4 +1,3 @@
@using BTCPayServer.Abstractions.Contracts
@model LoginViewModel
@inject BTCPayServer.Services.PoliciesSettings PoliciesSettings
@{

View file

@ -1,35 +1,17 @@
@using System.Net
@using System.Text.RegularExpressions
@model int?
@{
Layout = "_LayoutError";
ViewData["Title"] = "Generic Error occurred";
if (Model.HasValue)
{
var httpCode = (HttpStatusCode)Model.Value;
ViewData["Title"] = $"{(int)httpCode} - {httpCode.ToString()}";
var name = Regex.Replace(httpCode.ToString(), @"(\B[A-Z])", @" $1");
ViewData["Title"] = $"{(int)httpCode} - {name}";
}
}
<div class="row justify-content-center mb-2">
<div class="col text-center">
<a asp-controller="UIHome" asp-action="Index">
<img src="~/img/btcpay-logo.svg" alt="BTCPay Server" class="mb-4" height="70" asp-append-version="true"/>
</a>
<h1 class="h2 mb-3">@ViewData["Title"]</h1>
</div>
</div>
<p class="lead text-center">
Generic error occurred, HTTP Code: @Model
<br /><br />
Consult server log for more details.
<br /><br />
<a href="/">Navigate back to home</a>.
<br /><br />
</p>
<div class="row justify-content-center mt-5">
<div class="col">
<partial name="_BTCPaySupporters"/>
</div>
</div>
<p class="mt-4">A generic error occurred (HTTP Code: @Model)</p>
<p>Please consult the server log for more details.</p>
<a href="/">Navigate back to home</a>

View file

@ -2,30 +2,24 @@
Layout = "_LayoutSimple";
}
<div class="row">
<div class="col-12 col-head" style="justify-content:center; flex-direction: row; display:flex; flex-direction:row; text-align:center;">
<a asp-controller="UIHome" asp-action="Index">
<img src="~/img/btcpay-logo.svg" alt="BTCPay Server" class="head-logo" height="70" asp-append-version="true" />
</a>
<style>
.main-logo { height: 4rem; width: 18rem; }
.main-logo-btcpay .main-logo-btcpay--small { display: none; }
.lead img { max-width: 100%; }
</style>
<h1 class="text-uppercase mt-3 ms-4">@ViewData["Title"]</h1>
</div>
</div>
<center>
<hr class="primary" />
</center>
<p class="lead text-center">
@RenderBody()
</p>
<center>
<hr class="primary" />
</center>
<div class="row justify-content-center mt-5">
<div class="col">
<partial name="_BTCPaySupporters" />
<div class="text-center">
<a asp-controller="UIHome" asp-action="Index" class="d-inline-block navbar-brand mx-0 mb-5">
<vc:main-logo />
</a>
<h1 class="text-uppercase my-3">@ViewData["Title"]</h1>
<div class="lead text-center">
@RenderBody()
</div>
<hr class="primary my-5" />
<partial name="_BTCPaySupporters" />
</div>

View file

@ -175,7 +175,7 @@
</span>
@if (!Theme.CustomTheme)
{
<vc:theme-switch css-class="text-muted mx-2" responsive="none"/>
<vc:theme-switch css-class="text-muted mx-2" />
}
</div>
</footer>

View file

@ -381,7 +381,7 @@
</span>
@if (!Theme.CustomTheme)
{
<vc:theme-switch css-class="text-muted mx-2" responsive="none"/>
<vc:theme-switch css-class="text-muted mx-2" />
}
</div>
</footer>

View file

@ -230,7 +230,7 @@
</span>
@if (!Theme.CustomTheme)
{
<vc:theme-switch css-class="text-muted mx-2" responsive="none"/>
<vc:theme-switch css-class="text-muted mx-2" />
}
</div>
</footer>

View file

@ -1,6 +1,9 @@
@model BTCPayServer.Services.ThemeSettings
@using BTCPayServer.Abstractions.Contracts
@model BTCPayServer.Services.ThemeSettings
@inject IFileService FileService
@{
ViewData.SetActivePage(ServerNavPages.Theme, "Theme");
var canUpload = await FileService.IsAvailable();
}
@section PageFootContent {
@ -11,16 +14,12 @@
<div class="row">
<div class="col-xl-8 col-xxl-constrain">
<form method="post">
<form method="post" enctype="multipart/form-data">
<p>Use the default Light or Dark Themes, or provide a CSS theme file below.</p>
@if (!ViewContext.ModelState.IsValid)
{
<div asp-validation-summary="All" class="text-danger"></div>
}
<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>
<span asp-validation-for="CustomTheme" class="text-danger"></span>
</div>
<div class="collapse @(Model.CustomTheme ? "show" : "")" id="CustomThemeSettings">
<div class="form-group">
@ -30,9 +29,32 @@
</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>
</div>
<h3 class="mt-5 mb-3">Branding</h3>
<div class="form-group">
<label asp-for="LogoFile" class="form-label"></label>
@if (canUpload)
{
<div class="d-flex flex-wrap gap-3 align-items-center">
<input asp-for="LogoFile" type="file" class="form-control flex-grow">
@if (!string.IsNullOrEmpty(Model.LogoFileId))
{
<img src="@(await FileService.GetFileUrl(Context.Request.GetAbsoluteRootUri(), Model.LogoFileId))" alt="Logo" style="height:2.1rem;max-width:10.5rem;"/>
<button type="submit" class="btn btn-sm btn-outline-danger" name="RemoveLogoFile" value="true">Remove</button>
}
</div>
<span asp-validation-for="LogoFile" class="text-danger"></span>
}
else
{
<input asp-for="LogoFile" type="file" class="form-control" disabled>
<p class="form-text text-muted">In order to upload a logo, a <a asp-controller="UIServer" asp-action="Files">file storage</a> must be configured.</p>
}
</div>
<button type="submit" class="btn btn-primary mt-2" name="command" value="Save">Save</button>
</form>
</div>

View file

@ -33,7 +33,7 @@
<span asp-validation-for="StoreWebsite" class="text-danger"></span>
</div>
<h3 class="mb-3">Branding</h3>
<h3 class="mt-5 mb-3">Branding</h3>
<div class="form-group">
<label asp-for="LogoFile" class="form-label"></label>
@if (canUpload)

View file

@ -135,6 +135,17 @@
height: 1px;
}
#StoreSelectorHome {
position: relative;
}
#StoreSelectorHome .badge {
position: absolute;
top: .25rem;
left: 100%;
font-size: 9px;
}
#StoreSelectorDropdown,
#StoreSelectorToggle {
width: 100%;
@ -144,6 +155,7 @@
display: flex;
align-items: center;
color: var(--btcpay-header-link);
background-color: var(--btcpay-header-bg);
}
#StoreSelectorToggle::after {
@ -191,24 +203,35 @@
}
/* Logo */
#mainMenuHead .main-logo {
display: inline-block;
height: 2rem;
}
@media (max-width: 575px) {
.logo {
width: 1.125rem;
height: 2rem;
#mainMenuHead .main-logo-custom {
max-width: 25vw;
}
.logo-large {
#mainMenuHead .main-logo-btcpay {
width: 1.125rem;
}
#mainMenuHead .main-logo-btcpay .main-logo-btcpay--large {
display: none;
}
}
@media (min-width: 576px) {
.logo {
width: 4.6rem;
height: 2rem;
#mainMenuHead .main-logo-custom {
max-width: 10.5rem;
}
.logo-small {
#mainMenuHead .main-logo-btcpay {
width: 4.625rem;
}
#mainMenuHead .main-logo-btcpay .main-logo-btcpay--small {
display: none;
}
}