CosplayerDetail now fetches album pages from GET /api/album?personOwnerId= instead of re-fetching the entire PersonDetailedDto per scroll page. The home browse page opts out of the asset count query (includeCount=false).
528 lines
23 KiB
Plaintext
528 lines
23 KiB
Plaintext
@page "/cosplayer/{Id:guid}"
|
|
@using Butter.Dtos.Album
|
|
@using Butter.Dtos.Asset
|
|
@using Butter.Dtos.Person
|
|
@using Butter.Types
|
|
@using Microsoft.Extensions.Options
|
|
@using System.Globalization
|
|
@inject PersonService personService
|
|
@inject AlbumService albumService
|
|
@inject AssetService assetService
|
|
@inject LoginService loginService
|
|
@inject IOptions<ServiceOptions> serviceOptions
|
|
@inject NavigationManager navigationManager
|
|
|
|
<PageTitle>@(person?.Name ?? "Cosplayer")</PageTitle>
|
|
|
|
@if (isLoading) {
|
|
<LoadSpinner/>
|
|
} else if (person == null) {
|
|
<EmptyState Variant="danger" Title="Cosplayer not found">
|
|
<p>The cosplayer you are looking for does not exist or you do not have access to them.</p>
|
|
<a href="/cosplayers" class="btn btn-danger">Back to Cosplayers</a>
|
|
</EmptyState>
|
|
} else {
|
|
<div class="cosplayer-detail">
|
|
@* Banner *@
|
|
<div class="cosplayer-banner @(bannerCoverUrls.Count == 0 ? "cosplayer-banner-empty" : "")">
|
|
@if (bannerCoverUrls.Count > 0) {
|
|
<div class="cosplayer-banner-grid" style="grid-template-columns: repeat(@bannerCoverUrls.Count, 1fr)">
|
|
@foreach (var url in bannerCoverUrls) {
|
|
<div class="banner-cell" style="background-image: url('@url')"></div>
|
|
}
|
|
</div>
|
|
}
|
|
<div class="cosplayer-banner-overlay"></div>
|
|
<div class="cosplayer-banner-content">
|
|
<div class="cosplayer-avatar">
|
|
@if (person.ProfileAssetId.HasValue) {
|
|
var cx = person.ProfileCropX ?? 50;
|
|
var cy = person.ProfileCropY ?? 50;
|
|
var z = person.ProfileCropZoom ?? 0.6f;
|
|
<div class="cosplayer-avatar-clip">
|
|
<img src="@ThumbUrl(person.ProfileAssetId.Value)" alt=""
|
|
class="cosplayer-avatar-img"
|
|
style="width: calc(100% / @z.ToString(CultureInfo.InvariantCulture)); transform: translate(calc(-@cx.ToString(CultureInfo.InvariantCulture) * 1%), calc(-@cy.ToString(CultureInfo.InvariantCulture) * 1%))" />
|
|
</div>
|
|
} else {
|
|
<i class="bi bi-person-circle cosplayer-avatar-placeholder"></i>
|
|
}
|
|
</div>
|
|
<div>
|
|
<h2 class="cosplayer-name">@person.Name</h2>
|
|
<div class="cosplayer-stats">
|
|
<span><i class="bi bi-collection"></i> @person.TotalAlbums albums</span>
|
|
@if (selectMode && loginService.CanEdit(Id) && person != null && person.Albums != null) {
|
|
var pub = person.Albums.Count(a => a.Visibility == EVisibility.Public);
|
|
var prot = person.Albums.Count(a => a.Visibility == EVisibility.Protected);
|
|
var priv = person.Albums.Count(a => a.Visibility == EVisibility.Private);
|
|
<span class="mx-1">·</span>
|
|
<span class="text-muted small">@pub public, @prot protected, @priv private</span>
|
|
}
|
|
<span class="mx-1">·</span>
|
|
<span><i class="bi bi-image"></i> @person.TotalAssets assets</span>
|
|
@if (person.MaintainerUserIds?.Count > 0)
|
|
{
|
|
<span class="mx-1">·</span>
|
|
<span>
|
|
<i class="bi bi-people"></i>
|
|
Maintained by:
|
|
@for (var i = 0; i < person.MaintainerUsernames!.Count; i++)
|
|
{
|
|
<a href="/User/@person.MaintainerUserIds[i]" class="text-reset text-decoration-none">
|
|
@person.MaintainerUsernames[i]
|
|
</a>
|
|
@if (i < person.MaintainerUsernames.Count - 1)
|
|
{
|
|
<text>, </text>
|
|
}
|
|
}
|
|
</span>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@if (loginService.CanEdit(Id)) {
|
|
<button class="cosplayer-banner-edit" @onclick="OpenEditForm" title="Edit Cosplayer">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
@* Albums header *@
|
|
<div class="d-flex align-items-center mt-4 mb-2 px-2">
|
|
<h4 class="m-0"><i class="bi bi-collection"></i> Albums</h4>
|
|
</div>
|
|
|
|
<SortFilterBar SearchQuery="@albumSearchQuery"
|
|
SearchQueryChanged="@((string? v) => albumSearchQuery = v)"
|
|
SortBy="@albumSortBy"
|
|
SortByChanged="@((string? v) => albumSortBy = v)"
|
|
SortAsc="@albumSortAsc"
|
|
SortAscChanged="@((bool v) => albumSortAsc = v)"
|
|
SortOptions="@albumSortOptions">
|
|
<ActionButtons>
|
|
@if (person.TotalAlbums > 0) {
|
|
<div class="btn-group btn-group-sm d-none d-md-inline-flex">
|
|
<button class="btn @(viewMode == "masonry" ? "btn-primary" : "btn-secondary")"
|
|
@onclick="SetMasonry" title="Masonry">
|
|
<i class="bi bi-grid-3x2-gap"></i>
|
|
</button>
|
|
<button class="btn @(viewMode == "grid" ? "btn-primary" : "btn-secondary")"
|
|
@onclick="SetGrid" title="Grid">
|
|
<i class="bi bi-grid-3x3"></i>
|
|
</button>
|
|
</div>
|
|
}
|
|
@if (loginService.CanEdit(Id)) {
|
|
@* Desktop: inline buttons *@
|
|
<div class="d-none d-md-flex gap-1 align-items-center">
|
|
<button class="btn btn-success btn-sm" @onclick="OpenAlbumAssigner">
|
|
<i class="bi bi-plus-circle"></i> Add Albums
|
|
</button>
|
|
@if (person.TotalAlbums > 0) {
|
|
if (selectMode) {
|
|
<button class="btn btn-sm btn-danger" @onclick="DeleteSelectedAlbums" disabled="@(selectedAlbumIds.Count == 0)">
|
|
<i class="bi bi-trash"></i> Delete @selectedAlbumIds.Count
|
|
</button>
|
|
<button class="btn btn-sm btn-warning" @onclick="RemoveSelectedAlbums" disabled="@(selectedAlbumIds.Count == 0)">
|
|
<i class="bi bi-link-45deg"></i> Unlink @selectedAlbumIds.Count
|
|
</button>
|
|
<button class="btn btn-sm btn-secondary" @onclick="OpenVisibilityModal" disabled="@(selectedAlbumIds.Count == 0)">
|
|
<i class="bi bi-eye"></i> Visibility
|
|
</button>
|
|
<button class="btn btn-sm btn-info" @onclick="OpenAlbumMergeModal" disabled="@(selectedAlbumIds.Count < 2)">
|
|
<i class="bi bi-arrow-right-circle"></i> Merge
|
|
</button>
|
|
<button class="btn btn-sm btn-outline-secondary" @onclick="ToggleSelectAllAlbums">
|
|
@(albumsAllSelected ? "Deselect All" : "Select All")
|
|
</button>
|
|
<button class="btn btn-sm btn-primary" @onclick="CancelSelectMode">
|
|
Done
|
|
</button>
|
|
} else {
|
|
<button class="btn btn-secondary btn-sm" @onclick="() => selectMode = true">
|
|
<i class="bi bi-check2-square"></i> Select
|
|
</button>
|
|
}
|
|
}
|
|
<button class="btn btn-danger btn-sm" @onclick="ConfirmDelete" title="Delete cosplayer">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
@* Mobile: dropdown *@
|
|
<div class="btn-group btn-group-sm d-md-none">
|
|
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots-vertical"></i>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li>
|
|
<button class="dropdown-item" @onclick="OpenAlbumAssigner">
|
|
<i class="bi bi-plus-circle"></i> Add Albums
|
|
</button>
|
|
</li>
|
|
@if (person.TotalAlbums > 0) {
|
|
@if (selectMode) {
|
|
<li>
|
|
<button class="dropdown-item text-danger" @onclick="DeleteSelectedAlbums" disabled="@(selectedAlbumIds.Count == 0)">
|
|
<i class="bi bi-trash"></i> Delete @selectedAlbumIds.Count
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item text-warning" @onclick="RemoveSelectedAlbums" disabled="@(selectedAlbumIds.Count == 0)">
|
|
<i class="bi bi-link-45deg"></i> Unlink @selectedAlbumIds.Count
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item" @onclick="OpenVisibilityModal" disabled="@(selectedAlbumIds.Count == 0)">
|
|
<i class="bi bi-eye"></i> Visibility
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item" @onclick="OpenAlbumMergeModal" disabled="@(selectedAlbumIds.Count < 2)">
|
|
<i class="bi bi-arrow-right-circle"></i> Merge
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item" @onclick="ToggleSelectAllAlbums">
|
|
<i class="bi @(albumsAllSelected ? "bi-x-square" : "bi-check2-square")"></i> @(albumsAllSelected ? "Deselect All" : "Select All")
|
|
</button>
|
|
</li>
|
|
<li>
|
|
<button class="dropdown-item" @onclick="CancelSelectMode">
|
|
<i class="bi bi-x-circle"></i> Done
|
|
</button>
|
|
</li>
|
|
} else {
|
|
<li>
|
|
<button class="dropdown-item" @onclick="() => selectMode = true">
|
|
<i class="bi bi-check2-square"></i> Select
|
|
</button>
|
|
</li>
|
|
}
|
|
<li><hr class="dropdown-divider"></li>
|
|
}
|
|
<li>
|
|
<button class="dropdown-item text-danger" @onclick="ConfirmDelete">
|
|
<i class="bi bi-trash"></i> Delete Cosplayer
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
}
|
|
</ActionButtons>
|
|
</SortFilterBar>
|
|
|
|
<AlbumGrid @ref="personAlbumGridRef"
|
|
SearchQuery="@albumSearchQuery"
|
|
SortBy="@albumSortBy"
|
|
SortAsc="@albumSortAsc"
|
|
ViewMode="@viewMode"
|
|
SelectionMode="@selectMode"
|
|
SelectedIds="@selectedAlbumIds"
|
|
OnToggleSelection="@ToggleSelectAlbum"
|
|
LoadVersion="@albumLoadVersion"
|
|
FetchAlbums="@FetchPersonAlbums"
|
|
OnDataLoaded="@OnGridAlbumsLoaded" />
|
|
</div>
|
|
}
|
|
|
|
@if (showForm && person != null) {
|
|
<PersonForm Show="true"
|
|
EditPerson="new PersonPreviewDto { Id = person.Id, Name = person.Name, ProfileAssetId = person.ProfileAssetId, ProfileCropX = person.ProfileCropX, ProfileCropY = person.ProfileCropY, ProfileCropZoom = person.ProfileCropZoom }"
|
|
AssetIds="allAssetIds"
|
|
MaintainerUserIds="person.MaintainerUserIds"
|
|
OnSaved="HandleFormSaved"
|
|
OnClosed="CloseForm" />
|
|
}
|
|
|
|
<ModalFrame Title="" Show="showAlbumAssigner && person != null" OnClose="CloseAlbumAssigner">
|
|
<Body>
|
|
@if (person != null)
|
|
{
|
|
<h5>Assign Albums to @person.Name</h5>
|
|
@if (unassignedAlbums == null) {
|
|
<div class="text-muted">Loading...</div>
|
|
} else if (unassignedAlbums.Count == 0) {
|
|
<div class="text-muted">All albums are already assigned.</div>
|
|
} else {
|
|
<div class="border rounded p-2" style="max-height: 300px; overflow-y: auto;">
|
|
@foreach (var album in unassignedAlbums) {
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox"
|
|
id="@("aa-" + album.Id)"
|
|
checked="@assignAlbumIds.Contains(album.Id)"
|
|
@onchange="(e) => ToggleAssignAlbum(album.Id, (bool)(e.Value ?? false))" />
|
|
<label class="form-check-label" for="@("aa-" + album.Id)">
|
|
<span>@album.Name</span>
|
|
<span class="text-muted small ms-1">@album.AssetCount items</span>
|
|
</label>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</Body>
|
|
<Footer>
|
|
<button class="btn btn-secondary" @onclick="CloseAlbumAssigner">Cancel</button>
|
|
<button class="btn btn-success" @onclick="AssignSelectedAlbums" disabled="@(assignAlbumIds.Count == 0)">
|
|
Assign @assignAlbumIds.Count album(s)
|
|
</button>
|
|
</Footer>
|
|
</ModalFrame>
|
|
|
|
@if (showAlbumVisibilityModal) {
|
|
<VisibilityModal Show="true"
|
|
ItemCount="@selectedAlbumIds.Count"
|
|
ItemLabel="album(s)"
|
|
Visibility="@selectedAlbumVisibility"
|
|
VisibilityChanged="@((EVisibility v) => selectedAlbumVisibility = v)"
|
|
CascadeLabel="Also change visibility of all assets in these albums (skips private/deleted)"
|
|
CascadeEnabled="@cascadeToAssets"
|
|
CascadeEnabledChanged="@((bool v) => cascadeToAssets = v)"
|
|
OnApply="ApplyAlbumVisibility"
|
|
OnClose="CloseVisibilityModal" />
|
|
}
|
|
|
|
@if (showAlbumMergeModal) {
|
|
<MergeModal Show="true"
|
|
ItemLabel="Album"
|
|
Items="@albumMergeItems"
|
|
OnMergeConfirmed="@ExecuteMergeAlbums"
|
|
OnClose="() => showAlbumMergeModal = false" />
|
|
}
|
|
|
|
<ConfirmDialog Show="_showDeleteConfirm"
|
|
Title="Confirm Deletion"
|
|
Message="@_deleteConfirmMessage"
|
|
ConfirmText="Delete"
|
|
ConfirmButtonClass="btn-danger"
|
|
OnConfirm="ExecutePendingDelete"
|
|
OnCancel="() => _showDeleteConfirm = false" />
|
|
|
|
@code {
|
|
[Parameter]
|
|
public Guid Id { get; set; }
|
|
|
|
PersonDetailedDto? person;
|
|
AlbumGrid? personAlbumGridRef;
|
|
bool isLoading = true;
|
|
bool showForm;
|
|
bool selectMode;
|
|
bool showAlbumVisibilityModal;
|
|
bool showAlbumMergeModal;
|
|
List<MergeModal.MergeItem> albumMergeItems = [];
|
|
bool _showDeleteConfirm;
|
|
string _deleteConfirmMessage = "";
|
|
Func<Task>? _pendingDeleteAction;
|
|
bool cascadeToAssets;
|
|
EVisibility selectedAlbumVisibility = EVisibility.Public;
|
|
string token = string.Empty;
|
|
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
|
|
string viewMode = "masonry";
|
|
bool showAlbumAssigner;
|
|
List<AlbumPreviewDto>? unassignedAlbums;
|
|
HashSet<Guid> assignAlbumIds = [];
|
|
HashSet<Guid> selectedAlbumIds = [];
|
|
List<string> bannerCoverUrls = [];
|
|
|
|
// Search & sort
|
|
string? albumSearchQuery;
|
|
string? albumSortBy;
|
|
bool albumSortAsc = true;
|
|
|
|
Dictionary<string, string> albumSortOptions = new()
|
|
{
|
|
["Name"] = "name",
|
|
["Created"] = "created",
|
|
["Updated"] = "updated",
|
|
["Asset count"] = "assets"
|
|
};
|
|
|
|
int albumLoadVersion;
|
|
|
|
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
|
|
|
|
List<Guid>? allAssetIds => person?.Albums?
|
|
.SelectMany(a => Enumerable.Repeat(a.CoverAssetId ?? Guid.Empty, 1))
|
|
.Where(id => id != Guid.Empty)
|
|
.Distinct()
|
|
.ToList();
|
|
|
|
protected override async Task OnParametersSetAsync() {
|
|
token = loginService.AuthInfo?.Token ?? string.Empty;
|
|
await LoadPerson();
|
|
}
|
|
|
|
protected override void OnInitialized() {
|
|
loginService.LoggedUserChanged += (_, _) => StateHasChanged();
|
|
loginService.AuthInfoChanged += (_, info) => {
|
|
token = info?.Token ?? string.Empty;
|
|
};
|
|
}
|
|
|
|
async Task LoadPerson() {
|
|
isLoading = true;
|
|
person = await personService.GetByIdAsync(Id, 0, 30);
|
|
isLoading = false;
|
|
}
|
|
|
|
async Task OnGridAlbumsLoaded(List<AlbumPreviewDto> albums) {
|
|
bannerCoverUrls = albums
|
|
.Where(a => a.CoverAssetId.HasValue)
|
|
.OrderBy(_ => Random.Shared.Next())
|
|
.Take(3)
|
|
.Select(a => ThumbUrl(a.CoverAssetId!.Value))
|
|
.ToList();
|
|
}
|
|
|
|
async Task<List<AlbumPreviewDto>?> FetchPersonAlbums(int page, int pageSize, string? search, string? sortBy, bool sortAsc) {
|
|
return await albumService.GetAlbumsAsync(page, pageSize, search, sortBy, sortAsc, personOwnerId: Id);
|
|
}
|
|
|
|
void OpenEditForm() => showForm = true;
|
|
|
|
async Task HandleFormSaved() {
|
|
showForm = false;
|
|
await LoadPerson();
|
|
}
|
|
|
|
void SetMasonry() => viewMode = "masonry";
|
|
void SetGrid() => viewMode = "grid";
|
|
|
|
void CloseForm() => showForm = false;
|
|
|
|
async Task OpenAlbumAssigner() {
|
|
showAlbumAssigner = true;
|
|
unassignedAlbums = await albumService.GetUnassignedAsync();
|
|
assignAlbumIds.Clear();
|
|
}
|
|
|
|
void CloseAlbumAssigner() {
|
|
showAlbumAssigner = false;
|
|
unassignedAlbums = null;
|
|
}
|
|
|
|
void ToggleAssignAlbum(Guid id, bool selected) {
|
|
if (selected) assignAlbumIds.Add(id);
|
|
else assignAlbumIds.Remove(id);
|
|
}
|
|
|
|
bool albumsAllSelected => person?.Albums != null && person.Albums.Count > 0 && selectedAlbumIds.Count == person.Albums.Count;
|
|
|
|
void CancelSelectMode() {
|
|
selectMode = false;
|
|
selectedAlbumIds.Clear();
|
|
}
|
|
|
|
void OpenAlbumMergeModal() {
|
|
if (personAlbumGridRef is null) return;
|
|
albumMergeItems = personAlbumGridRef.GetCurrentAlbumPreviews()
|
|
.Where(a => selectedAlbumIds.Contains(a.Id))
|
|
.Select(a => new MergeModal.MergeItem { Id = a.Id, Name = a.Name, Subtitle = $"{a.AssetCount} asset{(a.AssetCount == 1 ? "" : "s")}" })
|
|
.ToList();
|
|
showAlbumMergeModal = true;
|
|
}
|
|
|
|
async Task ExecuteMergeAlbums((Guid DestinationId, List<Guid> SourceIds) merge) {
|
|
if (merge.SourceIds.Count == 0) return;
|
|
var dto = new AlbumMergeDto { DestinationId = merge.DestinationId, SourceIds = merge.SourceIds };
|
|
await albumService.MergeAlbumsAsync(dto);
|
|
showAlbumMergeModal = false;
|
|
CancelSelectMode();
|
|
await LoadPerson();
|
|
albumLoadVersion++;
|
|
}
|
|
|
|
void ToggleSelectAllAlbums() {
|
|
if (person?.Albums == null) return;
|
|
if (albumsAllSelected)
|
|
selectedAlbumIds.Clear();
|
|
else
|
|
selectedAlbumIds.UnionWith(person.Albums.Select(a => a.Id));
|
|
}
|
|
|
|
void ToggleSelectAlbum(Guid id) {
|
|
if (selectedAlbumIds.Contains(id)) selectedAlbumIds.Remove(id);
|
|
else selectedAlbumIds.Add(id);
|
|
}
|
|
|
|
async Task AssignSelectedAlbums() {
|
|
if (person == null) return;
|
|
foreach (var albumId in assignAlbumIds)
|
|
await albumService.AssignPersonAsync(albumId, person.Id);
|
|
showAlbumAssigner = false;
|
|
unassignedAlbums = null;
|
|
await LoadPerson();
|
|
albumLoadVersion++;
|
|
}
|
|
|
|
async Task RemoveSelectedAlbums() {
|
|
if (person == null) return;
|
|
foreach (var albumId in selectedAlbumIds)
|
|
await albumService.UnlinkPersonAsync(albumId);
|
|
CancelSelectMode();
|
|
await LoadPerson();
|
|
albumLoadVersion++;
|
|
}
|
|
|
|
async Task DeleteSelectedAlbums() {
|
|
if (person == null || selectedAlbumIds.Count == 0) return;
|
|
_deleteConfirmMessage = $"Delete {selectedAlbumIds.Count} selected album{(selectedAlbumIds.Count == 1 ? "" : "s")}? This cannot be undone.";
|
|
_pendingDeleteAction = ExecuteDeleteSelectedAlbums;
|
|
_showDeleteConfirm = true;
|
|
}
|
|
|
|
async Task ExecuteDeleteSelectedAlbums() {
|
|
if (person == null || selectedAlbumIds.Count == 0) return;
|
|
_showDeleteConfirm = false;
|
|
await albumService.BulkDeleteAlbumsAsync([.. selectedAlbumIds]);
|
|
CancelSelectMode();
|
|
await LoadPerson();
|
|
albumLoadVersion++;
|
|
}
|
|
|
|
void OpenVisibilityModal() {
|
|
showAlbumVisibilityModal = true;
|
|
selectedAlbumVisibility = EVisibility.Public;
|
|
}
|
|
|
|
void CloseVisibilityModal() {
|
|
showAlbumVisibilityModal = false;
|
|
cascadeToAssets = false;
|
|
}
|
|
|
|
async Task ApplyAlbumVisibility() {
|
|
if (person == null || selectedAlbumIds.Count == 0) return;
|
|
await albumService.BulkUpdateAlbumsAsync([.. selectedAlbumIds], new AlbumUpdateDto { Visibility = selectedAlbumVisibility });
|
|
if (cascadeToAssets)
|
|
await albumService.CascadeVisibilityToAssetsAsync([.. selectedAlbumIds], selectedAlbumVisibility);
|
|
showAlbumVisibilityModal = false;
|
|
cascadeToAssets = false;
|
|
CancelSelectMode();
|
|
await LoadPerson();
|
|
albumLoadVersion++;
|
|
}
|
|
|
|
async Task ConfirmDelete() {
|
|
if (person == null) return;
|
|
_deleteConfirmMessage = $"Delete cosplayer '{person.Name}'? This cannot be undone.";
|
|
_pendingDeleteAction = ExecuteDeletePerson;
|
|
_showDeleteConfirm = true;
|
|
}
|
|
|
|
async Task ExecuteDeletePerson() {
|
|
if (person == null) return;
|
|
_showDeleteConfirm = false;
|
|
await personService.DeletePersonAsync(person.Id);
|
|
navigationManager.NavigateTo("/cosplayers");
|
|
}
|
|
|
|
async Task ExecutePendingDelete() {
|
|
if (_pendingDeleteAction != null)
|
|
await _pendingDeleteAction();
|
|
}
|
|
|
|
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
|
|
}
|