feat(ui): extract CosplayerGrid to isolate grid re-renders from SortFilterBar
- Create CosplayerGrid.razor child component that manages its own data loading, pagination, infinite scroll via cosplayerObserver, and empty state - Cosplayers.razor now only holds header/SortFilterBar/form state - Filter param changes trigger reload via OnParametersSetAsync - LoadVersion parameter allows parent to trigger grid reload - Remove OnFilterChanged no-op handler - grid handles reloads via params
This commit is contained in:
@@ -5,114 +5,58 @@
|
|||||||
@using Butter.Types
|
@using Butter.Types
|
||||||
@using MilkStream.Client
|
@using MilkStream.Client
|
||||||
@using Microsoft.Extensions.Options
|
@using Microsoft.Extensions.Options
|
||||||
@using Microsoft.JSInterop
|
|
||||||
@using MilkStream.Client.Components.Layout
|
@using MilkStream.Client.Components.Layout
|
||||||
@using MilkStream.Client.Components.Shared
|
@using MilkStream.Client.Components.Shared
|
||||||
@using MilkStream.Client.Services
|
@using MilkStream.Client.Services
|
||||||
@using System.Globalization
|
@using System.Globalization
|
||||||
@implements IAsyncDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
@inject PersonService personService
|
@inject PersonService personService
|
||||||
@inject LoginService loginService
|
@inject LoginService loginService
|
||||||
@inject IOptions<ServiceOptions> serviceOptions
|
@inject IOptions<ServiceOptions> serviceOptions
|
||||||
@inject NavigationManager navigationManager
|
@inject NavigationManager navigationManager
|
||||||
@inject IJSRuntime jsRuntime
|
|
||||||
|
|
||||||
<PageTitle>Cosplayers</PageTitle>
|
<PageTitle>Cosplayers</PageTitle>
|
||||||
|
|
||||||
@if (isLoading) {
|
<div class="d-flex align-items-center justify-content-between mb-3 px-2 flex-wrap gap-2">
|
||||||
<LoadSpinner/>
|
<h4 class="m-0"><i class="bi bi-people"></i> Cosplayers</h4>
|
||||||
} else {
|
<div class="d-flex gap-2 flex-shrink-0 flex-wrap align-items-center">
|
||||||
<div class="d-flex align-items-center justify-content-between mb-3 px-2 flex-wrap gap-2">
|
@if (IsAdminOrCurator) {
|
||||||
<h4 class="m-0"><i class="bi bi-people"></i> Cosplayers</h4>
|
if (cosplayerSelectMode) {
|
||||||
<div class="d-flex gap-2 flex-shrink-0 flex-wrap align-items-center">
|
<button class="btn btn-danger btn-sm" @onclick="DeleteSelectedCosplayers" disabled="@(selectedCosplayerIds.Count == 0)">
|
||||||
@if (IsAdminOrCurator) {
|
<i class="bi bi-trash"></i> Delete @selectedCosplayerIds.Count
|
||||||
if (cosplayerSelectMode) {
|
</button>
|
||||||
<button class="btn btn-danger btn-sm" @onclick="DeleteSelectedCosplayers" disabled="@(selectedCosplayerIds.Count == 0)">
|
<button class="btn btn-secondary btn-sm" @onclick="() => { cosplayerSelectMode = false; selectedCosplayerIds.Clear(); }">
|
||||||
<i class="bi bi-trash"></i> Delete @selectedCosplayerIds.Count
|
Done
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-secondary btn-sm" @onclick="() => { cosplayerSelectMode = false; selectedCosplayerIds.Clear(); }">
|
} else {
|
||||||
Done
|
<button class="btn btn-secondary btn-sm" @onclick="() => cosplayerSelectMode = true">
|
||||||
</button>
|
<i class="bi bi-check2-square"></i> Select
|
||||||
} else {
|
|
||||||
<button class="btn btn-secondary btn-sm" @onclick="() => cosplayerSelectMode = true">
|
|
||||||
<i class="bi bi-check2-square"></i> Select
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
<button class="btn btn-primary btn-sm" @onclick="OpenCreateForm">
|
|
||||||
<i class="bi bi-plus-circle"></i> New Cosplayer
|
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
<button class="btn btn-primary btn-sm" @onclick="OpenCreateForm">
|
||||||
</div>
|
<i class="bi bi-plus-circle"></i> New Cosplayer
|
||||||
|
</button>
|
||||||
<SortFilterBar SearchQuery="@searchQuery"
|
|
||||||
SearchQueryChanged="@((string? v) => searchQuery = v)"
|
|
||||||
SortBy="@sortBy"
|
|
||||||
SortByChanged="@((string? v) => sortBy = v)"
|
|
||||||
SortAsc="@sortAsc"
|
|
||||||
SortAscChanged="@((bool v) => sortAsc = v)"
|
|
||||||
SortOptions="@sortOptions"
|
|
||||||
OnFilterChanged="@OnFilterChanged" />
|
|
||||||
|
|
||||||
@if (people.Count == 0 && !hasMore) {
|
|
||||||
<EmptyState Variant="warning" Title="No media available">
|
|
||||||
@if (loginService.IsLoggedIn) {
|
|
||||||
<p>There are no cosplayers yet.</p>
|
|
||||||
} else {
|
|
||||||
<p>There is no public media available. Logging in <i>might</i> make more content visible.</p>
|
|
||||||
}
|
|
||||||
</EmptyState>
|
|
||||||
} else {
|
|
||||||
<div class="cosplayers-grid">
|
|
||||||
@foreach (var person in people) {
|
|
||||||
<div class="cosplayer-card @(cosplayerSelectMode ? "cosplayer-card-select" : "")" @key="person.Id"
|
|
||||||
@onclick="() => HandleCardClick(person.Id)"
|
|
||||||
@onclick:preventDefault="true">
|
|
||||||
@if (person.ProfileAssetId.HasValue) {
|
|
||||||
var cx1 = person.ProfileCropX ?? 50;
|
|
||||||
var cy1 = person.ProfileCropY ?? 50;
|
|
||||||
var zoom1 = person.ProfileCropZoom ?? 0.6f;
|
|
||||||
<div class="cosplayer-card-img-wrap">
|
|
||||||
<img src="@ThumbUrl(person.ProfileAssetId.Value)" alt=""
|
|
||||||
class="cosplayer-card-img"
|
|
||||||
style="width: calc(100% / @zoom1.ToString(CultureInfo.InvariantCulture)); transform: translate(calc(-@cx1.ToString(CultureInfo.InvariantCulture) * 1%), calc(-@cy1.ToString(CultureInfo.InvariantCulture) * 1%))"
|
|
||||||
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
|
|
||||||
<div class="cosplayer-card-fallback" style="display:none">
|
|
||||||
<i class="bi bi-person-circle"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
<div class="cosplayer-card-img-wrap">
|
|
||||||
<div class="cosplayer-card-fallback">
|
|
||||||
<i class="bi bi-person-circle"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div class="cosplayer-card-info">
|
|
||||||
<span class="cosplayer-card-name">@person.Name</span>
|
|
||||||
<span class="cosplayer-card-count">@person.TotalAlbums albums</span>
|
|
||||||
</div>
|
|
||||||
@if (cosplayerSelectMode) {
|
|
||||||
<div class="cosplayer-card-check">
|
|
||||||
@if (selectedCosplayerIds.Contains(person.Id)) {
|
|
||||||
<i class="bi bi-check-circle-fill text-danger"></i>
|
|
||||||
} else {
|
|
||||||
<i class="bi bi-circle text-white"></i>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (isLoadingMore) {
|
<SortFilterBar SearchQuery="@searchQuery"
|
||||||
<LoadSpinner/>
|
SearchQueryChanged="@((string? v) => searchQuery = v)"
|
||||||
}
|
SortBy="@sortBy"
|
||||||
|
SortByChanged="@((string? v) => sortBy = v)"
|
||||||
|
SortAsc="@sortAsc"
|
||||||
|
SortAscChanged="@((bool v) => sortAsc = v)"
|
||||||
|
SortOptions="@sortOptions" />
|
||||||
|
|
||||||
<div @ref="sentinelRef" class="masonry-sentinel"></div>
|
<CosplayerGrid SearchQuery="@searchQuery"
|
||||||
}
|
SortBy="@sortBy"
|
||||||
}
|
SortAsc="@sortAsc"
|
||||||
|
SelectionMode="@cosplayerSelectMode"
|
||||||
|
SelectedIds="@selectedCosplayerIds"
|
||||||
|
OnCardClicked="@NavigateToPerson"
|
||||||
|
OnToggleSelection="@ToggleCosplayerSelection"
|
||||||
|
LoadVersion="@loadVersion" />
|
||||||
|
|
||||||
@if (showCreateForm) {
|
@if (showCreateForm) {
|
||||||
<PersonForm Show="true"
|
<PersonForm Show="true"
|
||||||
@@ -122,24 +66,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
List<PersonPreviewDto> people = [];
|
|
||||||
bool isLoading = true;
|
|
||||||
bool isLoadingMore;
|
|
||||||
bool hasMore = true;
|
|
||||||
int currentPage = 0;
|
|
||||||
string token = string.Empty;
|
|
||||||
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
|
|
||||||
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
|
|
||||||
bool IsAdminOrCurator => loginService.LoggedUser?.AccessLevel is EAccessLevel.Admin or EAccessLevel.Curator;
|
|
||||||
bool showCreateForm;
|
bool showCreateForm;
|
||||||
bool cosplayerSelectMode;
|
bool cosplayerSelectMode;
|
||||||
HashSet<Guid> selectedCosplayerIds = [];
|
HashSet<Guid> selectedCosplayerIds = [];
|
||||||
ElementReference sentinelRef;
|
bool IsAdminOrCurator => loginService.LoggedUser?.AccessLevel is EAccessLevel.Admin or EAccessLevel.Curator;
|
||||||
DotNetObjectReference<Cosplayers>? dotNetRef;
|
|
||||||
|
|
||||||
string? searchQuery;
|
string? searchQuery;
|
||||||
string? sortBy;
|
string? sortBy;
|
||||||
bool sortAsc = true;
|
bool sortAsc = true;
|
||||||
|
int loadVersion;
|
||||||
Dictionary<string, string> sortOptions = new()
|
Dictionary<string, string> sortOptions = new()
|
||||||
{
|
{
|
||||||
["Name"] = "name",
|
["Name"] = "name",
|
||||||
@@ -149,122 +84,41 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
protected override async Task OnInitializedAsync() {
|
||||||
loginService.LoggedUserChanged += OnLoggedUserChanged;
|
loginService.LoggedUserChanged += OnLoggedUserChanged;
|
||||||
loginService.AuthInfoChanged += OnAuthInfoChanged;
|
|
||||||
token = loginService.AuthInfo?.Token ?? string.Empty;
|
|
||||||
|
|
||||||
if (!loginService.IsLoggedIn) {
|
|
||||||
isLoading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await LoadFirstPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
|
||||||
if (people.Count > 0 && dotNetRef == null) {
|
|
||||||
dotNetRef = DotNetObjectReference.Create(this);
|
|
||||||
await jsRuntime.InvokeVoidAsync(
|
|
||||||
"cosplayerObserver.observeBottom", sentinelRef, dotNetRef
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task LoadFirstPage() {
|
|
||||||
isLoading = true;
|
|
||||||
var items = await personService.GetAllAsync(0, 30, searchQuery, sortBy, sortAsc);
|
|
||||||
if (items?.Count > 0) {
|
|
||||||
people = items;
|
|
||||||
currentPage = 1;
|
|
||||||
}
|
|
||||||
hasMore = items?.Count == 30;
|
|
||||||
isLoading = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JSInvokable]
|
|
||||||
public async Task LoadMoreCosplayers() {
|
|
||||||
if (isLoadingMore || !hasMore) return;
|
|
||||||
isLoadingMore = true;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
var items = await personService.GetAllAsync(currentPage, 30, searchQuery, sortBy, sortAsc);
|
|
||||||
if (items?.Count > 0) {
|
|
||||||
people.AddRange(items);
|
|
||||||
currentPage++;
|
|
||||||
}
|
|
||||||
hasMore = items?.Count == 30;
|
|
||||||
isLoadingMore = false;
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task OnFilterChanged() {
|
|
||||||
people.Clear();
|
|
||||||
currentPage = 0;
|
|
||||||
hasMore = true;
|
|
||||||
// Dispose old observer so it re-attaches after re-render
|
|
||||||
if (dotNetRef != null) {
|
|
||||||
await jsRuntime.InvokeVoidAsync("cosplayerObserver.dispose");
|
|
||||||
dotNetRef.Dispose();
|
|
||||||
dotNetRef = null;
|
|
||||||
}
|
|
||||||
await LoadFirstPage();
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OnLoggedUserChanged(object? _, UserInfoDto? _2) => StateHasChanged();
|
void OnLoggedUserChanged(object? _, UserInfoDto? _2) => StateHasChanged();
|
||||||
|
|
||||||
void OnAuthInfoChanged(object? _, AuthInfo? info) {
|
|
||||||
token = info?.Token ?? string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync() {
|
|
||||||
loginService.LoggedUserChanged -= OnLoggedUserChanged;
|
|
||||||
loginService.AuthInfoChanged -= OnAuthInfoChanged;
|
|
||||||
if (dotNetRef != null) {
|
|
||||||
await jsRuntime.InvokeVoidAsync("cosplayerObserver.dispose");
|
|
||||||
dotNetRef.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void NavigateToPerson(Guid id) {
|
void NavigateToPerson(Guid id) {
|
||||||
navigationManager.NavigateTo($"/cosplayer/{id}");
|
navigationManager.NavigateTo($"/cosplayer/{id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
void HandleCardClick(Guid id) {
|
|
||||||
if (cosplayerSelectMode) ToggleCosplayerSelection(id);
|
|
||||||
else NavigateToPerson(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
|
|
||||||
|
|
||||||
void OpenCreateForm() {
|
|
||||||
showCreateForm = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task HandleFormSaved() {
|
|
||||||
showCreateForm = false;
|
|
||||||
people.Clear();
|
|
||||||
currentPage = 0;
|
|
||||||
hasMore = true;
|
|
||||||
await LoadFirstPage();
|
|
||||||
}
|
|
||||||
|
|
||||||
void CloseForm() {
|
|
||||||
showCreateForm = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ToggleCosplayerSelection(Guid id) {
|
void ToggleCosplayerSelection(Guid id) {
|
||||||
if (selectedCosplayerIds.Contains(id)) selectedCosplayerIds.Remove(id);
|
if (selectedCosplayerIds.Contains(id)) selectedCosplayerIds.Remove(id);
|
||||||
else selectedCosplayerIds.Add(id);
|
else selectedCosplayerIds.Add(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void OpenCreateForm() {
|
||||||
|
showCreateForm = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task HandleFormSaved() {
|
||||||
|
showCreateForm = false;
|
||||||
|
loadVersion++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloseForm() {
|
||||||
|
showCreateForm = false;
|
||||||
|
}
|
||||||
|
|
||||||
async Task DeleteSelectedCosplayers() {
|
async Task DeleteSelectedCosplayers() {
|
||||||
foreach (var id in selectedCosplayerIds.ToList())
|
foreach (var id in selectedCosplayerIds.ToList())
|
||||||
await personService.DeletePersonAsync(id);
|
await personService.DeletePersonAsync(id);
|
||||||
selectedCosplayerIds.Clear();
|
selectedCosplayerIds.Clear();
|
||||||
cosplayerSelectMode = false;
|
cosplayerSelectMode = false;
|
||||||
people.Clear();
|
loadVersion++;
|
||||||
currentPage = 0;
|
}
|
||||||
hasMore = true;
|
|
||||||
await LoadFirstPage();
|
public void Dispose() {
|
||||||
|
loginService.LoggedUserChanged -= OnLoggedUserChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
190
MilkStream.Client/Components/Shared/CosplayerGrid.razor
Normal file
190
MilkStream.Client/Components/Shared/CosplayerGrid.razor
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@using Butter.Dtos.Person
|
||||||
|
@using Butter.Types
|
||||||
|
@using Microsoft.Extensions.Options
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using MilkStream.Client
|
||||||
|
@using MilkStream.Client.Components.Layout
|
||||||
|
@using MilkStream.Client.Services
|
||||||
|
@using System.Globalization
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
|
@inject PersonService personService
|
||||||
|
@inject LoginService loginService
|
||||||
|
@inject IOptions<ServiceOptions> serviceOptions
|
||||||
|
@inject IJSRuntime jsRuntime
|
||||||
|
|
||||||
|
@if (isLoading) {
|
||||||
|
<LoadSpinner/>
|
||||||
|
} else if (people.Count == 0 && !hasMore) {
|
||||||
|
<EmptyState Variant="warning" Title="No media available">
|
||||||
|
@if (loginService.IsLoggedIn) {
|
||||||
|
<p>There are no cosplayers yet.</p>
|
||||||
|
} else {
|
||||||
|
<p>There is no public media available. Logging in <i>might</i> make more content visible.</p>
|
||||||
|
}
|
||||||
|
</EmptyState>
|
||||||
|
} else {
|
||||||
|
<div class="cosplayers-grid">
|
||||||
|
@foreach (var person in people) {
|
||||||
|
<div class="cosplayer-card @(SelectionMode ? "cosplayer-card-select" : "")" @key="person.Id"
|
||||||
|
@onclick="() => HandleCardClick(person.Id)"
|
||||||
|
@onclick:preventDefault="true">
|
||||||
|
@if (person.ProfileAssetId.HasValue) {
|
||||||
|
var cx1 = person.ProfileCropX ?? 50;
|
||||||
|
var cy1 = person.ProfileCropY ?? 50;
|
||||||
|
var zoom1 = person.ProfileCropZoom ?? 0.6f;
|
||||||
|
<div class="cosplayer-card-img-wrap">
|
||||||
|
<img src="@ThumbUrl(person.ProfileAssetId.Value)" alt=""
|
||||||
|
class="cosplayer-card-img"
|
||||||
|
style="width: calc(100% / @zoom1.ToString(CultureInfo.InvariantCulture)); transform: translate(calc(-@cx1.ToString(CultureInfo.InvariantCulture) * 1%), calc(-@cy1.ToString(CultureInfo.InvariantCulture) * 1%))"
|
||||||
|
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
|
||||||
|
<div class="cosplayer-card-fallback" style="display:none">
|
||||||
|
<i class="bi bi-person-circle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
<div class="cosplayer-card-img-wrap">
|
||||||
|
<div class="cosplayer-card-fallback">
|
||||||
|
<i class="bi bi-person-circle"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="cosplayer-card-info">
|
||||||
|
<span class="cosplayer-card-name">@person.Name</span>
|
||||||
|
<span class="cosplayer-card-count">@person.TotalAlbums albums</span>
|
||||||
|
</div>
|
||||||
|
@if (SelectionMode) {
|
||||||
|
<div class="cosplayer-card-check">
|
||||||
|
@if (SelectedIds.Contains(person.Id)) {
|
||||||
|
<i class="bi bi-check-circle-fill text-danger"></i>
|
||||||
|
} else {
|
||||||
|
<i class="bi bi-circle text-white"></i>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isLoadingMore) {
|
||||||
|
<LoadSpinner/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div @ref="sentinelRef" class="masonry-sentinel"></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string? SearchQuery { get; set; }
|
||||||
|
[Parameter] public string? SortBy { get; set; }
|
||||||
|
[Parameter] public bool SortAsc { get; set; } = true;
|
||||||
|
[Parameter] public bool SelectionMode { get; set; }
|
||||||
|
[Parameter] public HashSet<Guid> SelectedIds { get; set; } = [];
|
||||||
|
[Parameter] public EventCallback<Guid> OnCardClicked { get; set; }
|
||||||
|
[Parameter] public EventCallback<Guid> OnToggleSelection { get; set; }
|
||||||
|
[Parameter] public int LoadVersion { get; set; }
|
||||||
|
|
||||||
|
List<PersonPreviewDto> people = [];
|
||||||
|
int currentPage = 0;
|
||||||
|
bool hasMore = true;
|
||||||
|
bool isLoading = true;
|
||||||
|
bool isLoadingMore;
|
||||||
|
ElementReference sentinelRef;
|
||||||
|
DotNetObjectReference<CosplayerGrid>? dotNetRef;
|
||||||
|
|
||||||
|
int? _prevLoadVersion;
|
||||||
|
string? _prevSearch;
|
||||||
|
string? _prevSortBy;
|
||||||
|
bool _prevSortAsc;
|
||||||
|
string _token = string.Empty;
|
||||||
|
string _apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
|
||||||
|
string TokenParam => string.IsNullOrEmpty(_token) ? "" : $"?token={_token}";
|
||||||
|
|
||||||
|
protected override void OnInitialized() {
|
||||||
|
loginService.AuthInfoChanged += OnAuthInfoChanged;
|
||||||
|
_token = loginService.AuthInfo?.Token ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnParametersSetAsync() {
|
||||||
|
bool filterChanged = _prevLoadVersion != LoadVersion
|
||||||
|
|| _prevSearch != SearchQuery
|
||||||
|
|| _prevSortBy != SortBy
|
||||||
|
|| _prevSortAsc != SortAsc;
|
||||||
|
|
||||||
|
_prevLoadVersion = LoadVersion;
|
||||||
|
_prevSearch = SearchQuery;
|
||||||
|
_prevSortBy = SortBy;
|
||||||
|
_prevSortAsc = SortAsc;
|
||||||
|
|
||||||
|
if (filterChanged)
|
||||||
|
await Reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||||
|
if (people.Count > 0 && dotNetRef == null) {
|
||||||
|
dotNetRef = DotNetObjectReference.Create(this);
|
||||||
|
await jsRuntime.InvokeVoidAsync(
|
||||||
|
"cosplayerObserver.observeBottom", sentinelRef, dotNetRef
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task Reload() {
|
||||||
|
await DisposeObserver();
|
||||||
|
people.Clear();
|
||||||
|
currentPage = 0;
|
||||||
|
hasMore = true;
|
||||||
|
isLoading = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var items = await personService.GetAllAsync(0, 30, SearchQuery, SortBy, SortAsc);
|
||||||
|
if (items?.Count > 0) {
|
||||||
|
people = items;
|
||||||
|
currentPage = 1;
|
||||||
|
}
|
||||||
|
hasMore = items?.Count == 30;
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task LoadMoreCosplayers() {
|
||||||
|
if (isLoadingMore || !hasMore) return;
|
||||||
|
isLoadingMore = true;
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var items = await personService.GetAllAsync(currentPage, 30, SearchQuery, SortBy, SortAsc);
|
||||||
|
if (items?.Count > 0) {
|
||||||
|
people.AddRange(items);
|
||||||
|
currentPage++;
|
||||||
|
}
|
||||||
|
hasMore = items?.Count == 30;
|
||||||
|
isLoadingMore = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleCardClick(Guid id) {
|
||||||
|
if (SelectionMode) {
|
||||||
|
OnToggleSelection.InvokeAsync(id);
|
||||||
|
} else {
|
||||||
|
OnCardClicked.InvokeAsync(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string ThumbUrl(Guid id) => $"{_apiBase}/api/media/thumb/{id}{TokenParam}";
|
||||||
|
|
||||||
|
void OnAuthInfoChanged(object? _, AuthInfo? info) {
|
||||||
|
_token = info?.Token ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task DisposeObserver() {
|
||||||
|
if (dotNetRef != null) {
|
||||||
|
await jsRuntime.InvokeVoidAsync("cosplayerObserver.dispose");
|
||||||
|
dotNetRef.Dispose();
|
||||||
|
dotNetRef = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync() {
|
||||||
|
loginService.AuthInfoChanged -= OnAuthInfoChanged;
|
||||||
|
await DisposeObserver();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user