File upload for store logo and user profile picture
This commit is contained in:
parent
908b0d8a37
commit
85643b8b2e
@ -1,10 +1,13 @@
|
||||
@attribute [Route(Routes.Store)]
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.UI.Components.Layout
|
||||
@using BTCPayApp.UI.Features
|
||||
@using BTCPayServer.Client
|
||||
@using BTCPayServer.Client.Models
|
||||
@using Microsoft.AspNetCore.StaticFiles
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
@inject IDataDirectoryProvider DataDirectoryProvider
|
||||
@inject IAccountManager AccountManager
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject IState<StoreState> StoreState
|
||||
@ -46,7 +49,7 @@
|
||||
</div>
|
||||
@if (_canView is true)
|
||||
{
|
||||
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="HandleValidSubmit" SuccessMessage="@_successMessage" ErrorMessage="@Error">
|
||||
<ValidationEditContext @ref="_validationEditContext" Model="Model" OnValidSubmit="HandleValidSubmit" SuccessMessage="@_successMessage" ErrorMessage="@Error" class="mb-5">
|
||||
<DataAnnotationsValidator/>
|
||||
<fieldset disabled="@(_canModify is not true)">
|
||||
<fieldset class="box">
|
||||
@ -62,6 +65,34 @@
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<h2>Branding</h2>
|
||||
<fieldset class="box">
|
||||
<div class="form-group">
|
||||
<label for="BrandColor" class="form-label">Brand Color</label>
|
||||
<div class="input-group">
|
||||
<InputText @bind-Value="Model.BrandColor" class="form-control form-control-color flex-grow-0" id="BrandColor" type="color" style="width:3rem" aria-describedby="BrandColorValue"/>
|
||||
<InputText @bind-Value="Model.BrandColor" class="form-control form-control-color flex-grow-0 font-monospace" pattern="^#[0-9a-fA-F]{6}$" style="width:5.5rem;font-size:0.9rem"/>
|
||||
</div>
|
||||
<ValidationMessage For="@(() => Model.BrandColor)"/>
|
||||
</div>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<label for="Logo" class="form-label">Logo</label>
|
||||
@if (!string.IsNullOrEmpty(Model.LogoUrl))
|
||||
{
|
||||
<button type="button" class="btn btn-link p-0 text-danger" @onclick="UnsetLogo">
|
||||
<Icon Symbol="cross" /> Remove
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<InputFile OnChange="LoadLogo" @key="@_inputFileId" id="Logo" class="form-control"/>
|
||||
@if (!string.IsNullOrEmpty(Model.LogoUrl))
|
||||
{
|
||||
<img src="@Model.LogoUrl" alt="Logo" class="logo"/>
|
||||
}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<h2>Payment</h2>
|
||||
<fieldset class="box">
|
||||
<div class="form-group">
|
||||
@ -179,8 +210,10 @@
|
||||
private ValidationEditContext? _validationEditContext;
|
||||
private StoreModel? Model { get; set; }
|
||||
private string? _successMessage;
|
||||
private string? _errorMessage;
|
||||
private bool? _canView;
|
||||
private bool? _canModify;
|
||||
private Guid _inputFileId = Guid.NewGuid();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@ -233,10 +266,29 @@
|
||||
_successMessage = null;
|
||||
|
||||
var store = Store!;
|
||||
|
||||
if (!string.IsNullOrEmpty(Model!.LogoPath))
|
||||
{
|
||||
var path = Model.LogoPath;
|
||||
var mimeType = GetContentType(path);
|
||||
var upload = await AccountManager.GetClient().UploadStoreLogo(store.Id, path, mimeType);
|
||||
Model.LogoUrl = upload.LogoUrl;
|
||||
// cleanup
|
||||
File.Delete(path);
|
||||
Model.LogoPath = null;
|
||||
_inputFileId = Guid.NewGuid();
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Model!.LogoUrl))
|
||||
{
|
||||
await AccountManager.GetClient().DeleteStoreLogo(store.Id);
|
||||
}
|
||||
|
||||
Dispatcher.Dispatch(new StoreState.UpdateStore(StoreId!, new UpdateStoreRequest
|
||||
{
|
||||
Name = Model!.Name,
|
||||
Website = Model.Website,
|
||||
BrandColor = Model.BrandColor,
|
||||
LogoUrl = Model.LogoUrl,
|
||||
DefaultCurrency = Model.DefaultCurrency,
|
||||
AnyoneCanCreateInvoice = Model.AnyoneCanCreateInvoice,
|
||||
NetworkFeeMode = Model.NetworkFeeMode,
|
||||
@ -249,8 +301,6 @@
|
||||
OnChainWithLnInvoiceFallback = Model.OnChainWithLnInvoiceFallback,
|
||||
PlaySoundOnPayment = Model.PlaySoundOnPayment,
|
||||
// pass these attributes to not reset them
|
||||
BrandColor = store.BrandColor,
|
||||
LogoUrl = store.LogoUrl,
|
||||
CssUrl = store.CssUrl,
|
||||
PaymentSoundUrl = store.PaymentSoundUrl,
|
||||
SupportUrl = store.SupportUrl,
|
||||
@ -281,6 +331,8 @@
|
||||
{
|
||||
Name = data.Name,
|
||||
Website = data.Website,
|
||||
BrandColor = data.BrandColor,
|
||||
LogoUrl = data.LogoUrl,
|
||||
DefaultCurrency = data.DefaultCurrency,
|
||||
AnyoneCanCreateInvoice = data.AnyoneCanCreateInvoice,
|
||||
NetworkFeeMode = data.NetworkFeeMode,
|
||||
@ -298,7 +350,46 @@
|
||||
private StoreData? Store => StoreState.Value.Store?.Data;
|
||||
private bool Loading => StoreState.Value.Store?.Loading is true;
|
||||
private bool Sending => StoreState.Value.Store?.Sending is true;
|
||||
private string? Error => StoreState.Value.Store?.Error;
|
||||
private string? Error => _errorMessage ?? StoreState.Value.Store?.Error;
|
||||
|
||||
private async Task LoadLogo(InputFileChangeEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var appData = await DataDirectoryProvider.GetAppDataDirectory();
|
||||
var fileName = e.File.Name;
|
||||
var dirPath = Path.Combine(appData, "tmp");
|
||||
var filePath = Path.Combine(dirPath, fileName);
|
||||
Directory.CreateDirectory(dirPath);
|
||||
|
||||
await using FileStream fs = new(filePath, FileMode.Create);
|
||||
await e.File.OpenReadStream().CopyToAsync(fs);
|
||||
await fs.FlushAsync();
|
||||
Model!.LogoPath = filePath;
|
||||
_errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"Profile picture could not be applied: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsetLogo()
|
||||
{
|
||||
Model!.LogoUrl = null;
|
||||
Model.LogoPath = null;
|
||||
_inputFileId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
private static string GetContentType(string filePath)
|
||||
{
|
||||
var mimeProvider = new FileExtensionContentTypeProvider();
|
||||
if (!mimeProvider.TryGetContentType(filePath, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
private class StoreModel
|
||||
{
|
||||
@ -307,6 +398,12 @@
|
||||
[Url]
|
||||
public string? Website { get; set; }
|
||||
|
||||
// Branding
|
||||
public string? BrandColor { get; set; }
|
||||
[Url]
|
||||
public string? LogoUrl { get; set; }
|
||||
public string? LogoPath { get; set; }
|
||||
|
||||
// Payment
|
||||
[Required]
|
||||
public string? DefaultCurrency { get; set; }
|
||||
|
||||
6
BTCPayApp.UI/Pages/Settings/StorePage.razor.css
Normal file
6
BTCPayApp.UI/Pages/Settings/StorePage.razor.css
Normal file
@ -0,0 +1,6 @@
|
||||
.logo {
|
||||
height: var(--profile-picture-size, 2.1rem);
|
||||
width: var(--profile-picture-size, 2.1rem);
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
@ -3,8 +3,11 @@
|
||||
@using BTCPayApp.CommonServer.Models
|
||||
@using BTCPayApp.Core
|
||||
@using BTCPayApp.Core.Auth
|
||||
@using BTCPayApp.Core.Contracts
|
||||
@using BTCPayApp.UI.Components.Layout
|
||||
@using Microsoft.AspNetCore.StaticFiles
|
||||
@inject IAccountManager AccountManager
|
||||
@inject IDataDirectoryProvider DataDirectoryProvider
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
|
||||
<PageTitle>User</PageTitle>
|
||||
@ -28,7 +31,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="box">
|
||||
<ValidationEditContext Model="Account" OnValidSubmit="HandleValidAccountSubmit" SuccessMessage="@_accountSuccessMessage" ErrorMessage="@_accountErrorMessage">
|
||||
<ValidationEditContext Model="Account" OnValidSubmit="HandleValidAccountSubmit" SuccessMessage="@_accountSuccessMessage" ErrorMessage="@_accountErrorMessage" enctype="multipart/form-data">
|
||||
<DataAnnotationsValidator />
|
||||
<div class="form-group">
|
||||
<label for="Email" class="form-label" data-required>Email</label>
|
||||
@ -41,10 +44,25 @@
|
||||
<ValidationMessage For="@(() => Account.Name)" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="ImageUrl" class="form-label">Image Url</label>
|
||||
<InputText @bind-Value="Account.ImageUrl" id="ImageUrl" type="url" class="form-control"/>
|
||||
<div class="d-flex align-items-center justify-content-between gap-2">
|
||||
<label for="ProfilePicture" class="form-label">Profile Picture</label>
|
||||
@if (!string.IsNullOrEmpty(Account.ImageUrl))
|
||||
{
|
||||
<button type="button" class="btn btn-link p-0 text-danger" @onclick="UnsetProfilePicture">
|
||||
<Icon Symbol="cross" /> Remove
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<InputFile OnChange="LoadProfilePicture" @key="@_inputFileId" id="ProfilePicture" class="form-control"/>
|
||||
@if (!string.IsNullOrEmpty(Account.ImageUrl))
|
||||
{
|
||||
<img src="@Account.ImageUrl" alt="Profile picture" class="profile-picture"/>
|
||||
}
|
||||
</div>
|
||||
<ValidationMessage For="@(() => Account.ImageUrl)" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-100" disabled="@(_accountSending)">
|
||||
@if (_accountSending)
|
||||
{
|
||||
@ -115,6 +133,7 @@
|
||||
private bool _passwordSending;
|
||||
private string? _passwordErrorMessage;
|
||||
private string? _passwordSuccessMessage;
|
||||
private Guid _inputFileId = Guid.NewGuid();
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
@ -148,6 +167,22 @@
|
||||
_accountErrorMessage = _accountSuccessMessage = null;
|
||||
_accountSending = true;
|
||||
|
||||
if (!string.IsNullOrEmpty(Account!.ImagePath))
|
||||
{
|
||||
var path = Account.ImagePath;
|
||||
var mimeType = GetContentType(path);
|
||||
var upload = await AccountManager.GetClient().UploadCurrentUserProfilePicture(path, mimeType);
|
||||
Account.ImageUrl = upload.ImageUrl;
|
||||
// cleanup
|
||||
File.Delete(path);
|
||||
Account.ImagePath = null;
|
||||
_inputFileId = Guid.NewGuid();
|
||||
}
|
||||
else if (string.IsNullOrEmpty(Account!.ImageUrl))
|
||||
{
|
||||
await AccountManager.GetClient().DeleteCurrentUserProfilePicture();
|
||||
}
|
||||
|
||||
var result = await AccountManager.ChangeAccountInfo(Account.Email!, Account.Name, Account.ImageUrl);
|
||||
_accountSending = false;
|
||||
|
||||
@ -187,6 +222,45 @@
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadProfilePicture(InputFileChangeEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var appData = await DataDirectoryProvider.GetAppDataDirectory();
|
||||
var fileName = e.File.Name;
|
||||
var dirPath = Path.Combine(appData, "tmp");
|
||||
var filePath = Path.Combine(dirPath, fileName);
|
||||
Directory.CreateDirectory(dirPath);
|
||||
|
||||
await using FileStream fs = new(filePath, FileMode.Create);
|
||||
await e.File.OpenReadStream().CopyToAsync(fs);
|
||||
await fs.FlushAsync();
|
||||
Account!.ImagePath = filePath;
|
||||
_accountErrorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_accountErrorMessage = $"Profile picture could not be applied: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsetProfilePicture()
|
||||
{
|
||||
Account!.ImageUrl = null;
|
||||
Account.ImagePath = null;
|
||||
_inputFileId = Guid.NewGuid();
|
||||
}
|
||||
|
||||
private static string GetContentType(string filePath)
|
||||
{
|
||||
var mimeProvider = new FileExtensionContentTypeProvider();
|
||||
if (!mimeProvider.TryGetContentType(filePath, out var contentType))
|
||||
{
|
||||
contentType = "application/octet-stream";
|
||||
}
|
||||
return contentType;
|
||||
}
|
||||
|
||||
private class AccountModel
|
||||
{
|
||||
[Required, EmailAddress]
|
||||
@ -194,6 +268,7 @@
|
||||
public string? Name { get; set; }
|
||||
[Url]
|
||||
public string? ImageUrl { get; set; }
|
||||
public string? ImagePath { get; set; }
|
||||
}
|
||||
|
||||
private class PasswordModel
|
||||
|
||||
Loading…
Reference in New Issue
Block a user