@page "/" @using Butter.Dtos.Asset @using Butter.Types @using Microsoft.Extensions.Options @using Microsoft.JSInterop @using MilkStream.Client.Components.Layout @using MilkStream.Client.Services @implements IAsyncDisposable @inject AssetService assetService @inject LoginService loginService @inject IJSRuntime jsRuntime @inject IOptions serviceOptions Home @if (isLoading) { } else if (flatList.Count == 0) {

No media available

@if (loginService.IsLoggedIn) {

You are logged in, but there is no media available at the moment.

} else {

Please log-in to be able to browse any media.

}
} else {
@{ var idx = 0; } @foreach (var page in allAssetPages) { @foreach (var asset in page) { var i = idx++;
@if (asset.HasThumbnail) { } else {
}
} }
@if (isLoadingMore) { }
} @if (selectedAsset != null) {
@if (selectedIndex > 0) { } @if (selectedIndex < flatList.Count - 1) { } @if (selectedAsset.HasPreview) { } else { }
} @code { List> allAssetPages = []; List flatList = []; int currentPage = 1; bool hasMore = true; bool isLoading = true; bool isLoadingMore; AssetPreviewDto? selectedAsset; int selectedIndex = -1; ElementReference sentinelRef; DotNetObjectReference? dotNetRef; string token = string.Empty; string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/'); string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}"; protected override async Task OnInitializedAsync() { loginService.LoggedUserChanged += (_, _) => StateHasChanged(); loginService.AuthInfoChanged += (_, info) => { token = info?.Token ?? string.Empty; StateHasChanged(); }; token = loginService.AuthInfo?.Token ?? string.Empty; await LoadFirstPage(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (flatList.Count > 0 && hasMore && dotNetRef == null) { dotNetRef = DotNetObjectReference.Create(this); await jsRuntime.InvokeVoidAsync( "masonryObserver.observe", sentinelRef, dotNetRef ); } if (selectedAsset != null) await jsRuntime.InvokeVoidAsync("masonryObserver.lockBodyScroll"); else await jsRuntime.InvokeVoidAsync("masonryObserver.unlockBodyScroll"); } async Task LoadFirstPage() { isLoading = true; var items = await assetService.GetAssetsAsync(EAssetType.Image, true, 1, 30); if (items?.Count > 0) { allAssetPages.Add(items); RebuildFlatList(); currentPage = 2; } hasMore = items?.Count == 30; isLoading = false; } [JSInvokable] public async Task LoadNextPage() { if (isLoadingMore || !hasMore) return; isLoadingMore = true; StateHasChanged(); var items = await assetService.GetAssetsAsync(EAssetType.Image, true, currentPage, 30); if (items?.Count > 0) { allAssetPages.Add(items); RebuildFlatList(); currentPage++; if (allAssetPages.Count > 10) allAssetPages.RemoveAt(0); } hasMore = items?.Count == 30; isLoadingMore = false; StateHasChanged(); } void RebuildFlatList() => flatList = allAssetPages.SelectMany(p => p).ToList(); void OpenPreview(AssetPreviewDto asset, int index) { selectedAsset = asset; selectedIndex = index; } void ClosePreview() { selectedAsset = null; selectedIndex = -1; } void NavigatePreview(int direction) { var newIndex = selectedIndex + direction; if (newIndex < 0 || newIndex >= flatList.Count) return; selectedAsset = flatList[newIndex]; selectedIndex = newIndex; } void OnPreviewKeyDown(KeyboardEventArgs e) { switch (e.Key) { case "Escape": ClosePreview(); break; case "ArrowLeft": NavigatePreview(-1); break; case "ArrowRight": NavigatePreview(1); break; } } static string GetAspectRatio(AssetPreviewDto asset) { if (asset.ResolutionWidth > 0 && asset.ResolutionHeight > 0) return $"{asset.ResolutionWidth} / {asset.ResolutionHeight}"; return "4 / 3"; } string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}"; string PreviewUrl(Guid id) => $"{apiBase}/api/media/preview/{id}{TokenParam}"; public async ValueTask DisposeAsync() { if (dotNetRef != null) { await jsRuntime.InvokeVoidAsync("masonryObserver.dispose"); dotNetRef.Dispose(); } } }