From 17f3b4125b108828cfc882fa079f2b3060c0fd76 Mon Sep 17 00:00:00 2001 From: d11n Date: Mon, 14 Nov 2022 14:29:23 +0100 Subject: [PATCH] Add option to customize the instance logo (#4258) * Add option to customize the instance logo Custom logo for BTCPay instances * Incorporate SVGUse helper --- .../Components/MainLogo/Default.cshtml | 20 ++++++ BTCPayServer/Components/MainLogo/MainLogo.cs | 16 +++++ .../Components/MainLogo/MainLogoViewModel.cs | 7 ++ .../Components/StoreSelector/Default.cshtml | 18 ++--- .../Controllers/UIServerController.Storage.cs | 8 +-- .../Controllers/UIServerController.cs | 69 ++++++++++++++++--- BTCPayServer/Services/ThemesSettings.cs | 8 +++ .../Crowdfund/Public/ViewCrowdfund.cshtml | 2 +- .../Views/Shared/_LayoutSignedOut.cshtml | 8 ++- BTCPayServer/Views/UIAccount/Login.cshtml | 1 - BTCPayServer/Views/UIError/Handle.cshtml | 32 ++------- .../Views/UIError/_LayoutError.cshtml | 42 +++++------ .../Views/UIInvoice/InvoiceReceipt.cshtml | 2 +- .../ViewPaymentRequest.cshtml | 2 +- .../UIPullPayment/ViewPullPayment.cshtml | 2 +- BTCPayServer/Views/UIServer/Theme.cshtml | 36 ++++++++-- .../Views/UIStores/GeneralSettings.cshtml | 2 +- BTCPayServer/wwwroot/main/layout.css | 39 ++++++++--- 18 files changed, 217 insertions(+), 97 deletions(-) create mode 100644 BTCPayServer/Components/MainLogo/Default.cshtml create mode 100644 BTCPayServer/Components/MainLogo/MainLogo.cs create mode 100644 BTCPayServer/Components/MainLogo/MainLogoViewModel.cs diff --git a/BTCPayServer/Components/MainLogo/Default.cshtml b/BTCPayServer/Components/MainLogo/Default.cshtml new file mode 100644 index 000000000..0b29a52ba --- /dev/null +++ b/BTCPayServer/Components/MainLogo/Default.cshtml @@ -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); + +} +else +{ + +} diff --git a/BTCPayServer/Components/MainLogo/MainLogo.cs b/BTCPayServer/Components/MainLogo/MainLogo.cs new file mode 100644 index 000000000..fdd3a008e --- /dev/null +++ b/BTCPayServer/Components/MainLogo/MainLogo.cs @@ -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); + } + } +} diff --git a/BTCPayServer/Components/MainLogo/MainLogoViewModel.cs b/BTCPayServer/Components/MainLogo/MainLogoViewModel.cs new file mode 100644 index 000000000..7b6781a66 --- /dev/null +++ b/BTCPayServer/Components/MainLogo/MainLogoViewModel.cs @@ -0,0 +1,7 @@ +namespace BTCPayServer.Components.MainLogo +{ + public class MainLogoViewModel + { + public string CssClass { get; set; } + } +} diff --git a/BTCPayServer/Components/StoreSelector/Default.cshtml b/BTCPayServer/Components/StoreSelector/Default.cshtml index bb6858cc2..6ffccb695 100644 --- a/BTCPayServer/Components/StoreSelector/Default.cshtml +++ b/BTCPayServer/Components/StoreSelector/Default.cshtml @@ -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 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"; - + @if (Env.NetworkType != NBitcoin.ChainName.Mainnet) { - @Env.NetworkType.ToString() + var type = Env.NetworkType.ToString(); + @type.Replace("Testnet", "TN").Replace("Regtest", "RT") } } - 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) { - @{await LogoContent();} + @{await LogoContent();} } else if (Model.CurrentStoreIsOwner) { - @{await LogoContent();} + @{await LogoContent();} } else { - @{await LogoContent();} + @{await LogoContent();} }
diff --git a/BTCPayServer/Controllers/UIServerController.Storage.cs b/BTCPayServer/Controllers/UIServerController.Storage.cs index b104ff500..eced6e0e4 100644 --- a/BTCPayServer/Controllers/UIServerController.Storage.cs +++ b/BTCPayServer/Controllers/UIServerController.Storage.cs @@ -41,7 +41,7 @@ namespace BTCPayServer.Controllers Dictionary directUrlByFiles = new Dictionary(); 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(), @@ -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); } diff --git a/BTCPayServer/Controllers/UIServerController.cs b/BTCPayServer/Controllers/UIServerController.cs index 94a2330ae..b24d5e834 100644 --- a/BTCPayServer/Controllers/UIServerController.cs +++ b/BTCPayServer/Controllers/UIServerController.cs @@ -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 _externalServiceOptions; private readonly Logs Logs; private readonly StoredFileRepository _StoredFileRepository; - private readonly FileService _FileService; + private readonly IFileService _fileService; private readonly IEnumerable _StorageProviderServices; private readonly LinkGenerator _linkGenerator; private readonly EmailSenderFactory _emailSenderFactory; @@ -70,7 +71,7 @@ namespace BTCPayServer.Controllers UserManager userManager, UserService userService, StoredFileRepository storedFileRepository, - FileService fileService, + IFileService fileService, IEnumerable 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 Theme() { var data = await _SettingsRepository.GetSettingAsync() ?? new ThemeSettings(); return View(data); } - [Route("server/theme")] - [HttpPost] - public async Task Theme(ThemeSettings settings) + [HttpPost("server/theme")] + public async Task Theme(ThemeSettings model, [FromForm] bool RemoveLogoFile) { - if (settings.CustomTheme && !Uri.IsWellFormedUriString(settings.CssUri, UriKind.RelativeOrAbsolute)) + var settingsChanged = false; + var settings = await _SettingsRepository.GetSettingAsync() ?? 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 Emails() { diff --git a/BTCPayServer/Services/ThemesSettings.cs b/BTCPayServer/Services/ThemesSettings.cs index 2c0fbd0af..108738ee3 100644 --- a/BTCPayServer/Services/ThemesSettings.cs +++ b/BTCPayServer/Services/ThemesSettings.cs @@ -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 diff --git a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml index 49f250a8c..fc3536fe1 100644 --- a/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml +++ b/BTCPayServer/Views/Shared/Crowdfund/Public/ViewCrowdfund.cshtml @@ -266,7 +266,7 @@
Updated @Model.Info.LastUpdated
@if (!Theme.CustomTheme) { - + }
diff --git a/BTCPayServer/Views/Shared/_LayoutSignedOut.cshtml b/BTCPayServer/Views/Shared/_LayoutSignedOut.cshtml index ae3453fa7..a3b253177 100644 --- a/BTCPayServer/Views/Shared/_LayoutSignedOut.cshtml +++ b/BTCPayServer/Views/Shared/_LayoutSignedOut.cshtml @@ -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; } @await RenderSectionAsync("PageHeadContent", false) } @@ -28,8 +30,8 @@
- - BTCPay Server + +

Welcome to your BTCPay Server

diff --git a/BTCPayServer/Views/UIAccount/Login.cshtml b/BTCPayServer/Views/UIAccount/Login.cshtml index 63c20d2bd..da11f7aa5 100644 --- a/BTCPayServer/Views/UIAccount/Login.cshtml +++ b/BTCPayServer/Views/UIAccount/Login.cshtml @@ -1,4 +1,3 @@ -@using BTCPayServer.Abstractions.Contracts @model LoginViewModel @inject BTCPayServer.Services.PoliciesSettings PoliciesSettings @{ diff --git a/BTCPayServer/Views/UIError/Handle.cshtml b/BTCPayServer/Views/UIError/Handle.cshtml index aab073c7e..9c6308c96 100644 --- a/BTCPayServer/Views/UIError/Handle.cshtml +++ b/BTCPayServer/Views/UIError/Handle.cshtml @@ -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}"; } } -
-
- - BTCPay Server - - -

@ViewData["Title"]

-
-
- -

- Generic error occurred, HTTP Code: @Model -

- Consult server log for more details. -

- Navigate back to home. -

-

- -
-
- -
-
+

A generic error occurred (HTTP Code: @Model)

+

Please consult the server log for more details.

+Navigate back to home diff --git a/BTCPayServer/Views/UIError/_LayoutError.cshtml b/BTCPayServer/Views/UIError/_LayoutError.cshtml index 3fe8fb9a1..b706fc504 100644 --- a/BTCPayServer/Views/UIError/_LayoutError.cshtml +++ b/BTCPayServer/Views/UIError/_LayoutError.cshtml @@ -2,30 +2,24 @@ Layout = "_LayoutSimple"; } -
-
- - - + -

@ViewData["Title"]

-
-
- -
-
-
- -

- @RenderBody() -

- -
-
-
- -
-
- +
+ + + + +

@ViewData["Title"]

+ +
+ @RenderBody()
+ +
+ +
diff --git a/BTCPayServer/Views/UIInvoice/InvoiceReceipt.cshtml b/BTCPayServer/Views/UIInvoice/InvoiceReceipt.cshtml index d4d4713de..422cdaedb 100644 --- a/BTCPayServer/Views/UIInvoice/InvoiceReceipt.cshtml +++ b/BTCPayServer/Views/UIInvoice/InvoiceReceipt.cshtml @@ -175,7 +175,7 @@ @if (!Theme.CustomTheme) { - + }
diff --git a/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml b/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml index ecaf3f264..7d75575b0 100644 --- a/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml +++ b/BTCPayServer/Views/UIPaymentRequest/ViewPaymentRequest.cshtml @@ -381,7 +381,7 @@ @if (!Theme.CustomTheme) { - + }
diff --git a/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml b/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml index 5532e940c..f99d23b57 100644 --- a/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml +++ b/BTCPayServer/Views/UIPullPayment/ViewPullPayment.cshtml @@ -230,7 +230,7 @@ @if (!Theme.CustomTheme) { - + }
diff --git a/BTCPayServer/Views/UIServer/Theme.cshtml b/BTCPayServer/Views/UIServer/Theme.cshtml index aef63961d..558b75769 100644 --- a/BTCPayServer/Views/UIServer/Theme.cshtml +++ b/BTCPayServer/Views/UIServer/Theme.cshtml @@ -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 @@
-
+

Use the default Light or Dark Themes, or provide a CSS theme file below.

- @if (!ViewContext.ModelState.IsValid) - { -
- } +
-
@@ -30,9 +29,32 @@ +
+

Branding

+
+ + @if (canUpload) + { +
+ + @if (!string.IsNullOrEmpty(Model.LogoFileId)) + { + Logo + + } +
+ + } + else + { + +

In order to upload a logo, a file storage must be configured.

+ } +
+
diff --git a/BTCPayServer/Views/UIStores/GeneralSettings.cshtml b/BTCPayServer/Views/UIStores/GeneralSettings.cshtml index c0d127a27..db07c179f 100644 --- a/BTCPayServer/Views/UIStores/GeneralSettings.cshtml +++ b/BTCPayServer/Views/UIStores/GeneralSettings.cshtml @@ -33,7 +33,7 @@
-

Branding

+

Branding

@if (canUpload) diff --git a/BTCPayServer/wwwroot/main/layout.css b/BTCPayServer/wwwroot/main/layout.css index c4cce2c91..0a0479ff6 100644 --- a/BTCPayServer/wwwroot/main/layout.css +++ b/BTCPayServer/wwwroot/main/layout.css @@ -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; } }