- MediaController: accept ?token=<jwt> query param as alt auth for browser img requests, validating against signing key via JwtSecurityTokenHandler - Home.razor: append current JWT token to all thumbnail/preview URLs, update on AuthInfoChanged, empty when not logged in
204 lines
6.9 KiB
Plaintext
204 lines
6.9 KiB
Plaintext
@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> serviceOptions
|
|
|
|
<PageTitle>Home</PageTitle>
|
|
|
|
@if (isLoading) {
|
|
<LoadSpinner/>
|
|
} else if (flatList.Count == 0) {
|
|
<div class="border-warning border-5 border-opacity-100 rounded-3 p-3 m-5 bg-warning-subtle text-center">
|
|
<h2 class="text-warning">No media available</h2>
|
|
@if (loginService.IsLoggedIn) {
|
|
<p class="text-warning-emphasis">You are logged in, but there is no media available at the moment.</p>
|
|
} else {
|
|
<p class="text-warning-emphasis">Please <a href="/login" class="link-warning">log-in</a> to be able to browse any media.</p>
|
|
}
|
|
</div>
|
|
} else {
|
|
<div class="masonry-grid">
|
|
@{ var idx = 0; }
|
|
@foreach (var page in allAssetPages) {
|
|
@foreach (var asset in page) {
|
|
var i = idx++;
|
|
<div class="masonry-tile" @key="asset.Id"
|
|
style="aspect-ratio:@GetAspectRatio(asset)"
|
|
@onclick="() => OpenPreview(asset, i)">
|
|
@if (asset.HasThumbnail) {
|
|
<img src="@ThumbUrl(asset.Id)"
|
|
loading="lazy" alt=""
|
|
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
|
|
<div class="tile-fallback" style="display:none">
|
|
<i class="bi bi-image"></i>
|
|
</div>
|
|
} else {
|
|
<div class="tile-fallback">
|
|
<i class="bi bi-image"></i>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
@if (isLoadingMore) {
|
|
<LoadSpinner/>
|
|
}
|
|
|
|
<div @ref="sentinelRef" class="masonry-sentinel"></div>
|
|
}
|
|
|
|
@if (selectedAsset != null) {
|
|
<div class="preview-overlay" @onkeydown="OnPreviewKeyDown" tabindex="0" @onclick="ClosePreview">
|
|
<div class="preview-content" @onclick:stopPropagation="true">
|
|
<button class="preview-close" @onclick="ClosePreview">×</button>
|
|
|
|
@if (selectedIndex > 0) {
|
|
<button class="preview-nav preview-prev" @onclick="() => NavigatePreview(-1)" @onclick:stopPropagation="true">‹</button>
|
|
}
|
|
@if (selectedIndex < flatList.Count - 1) {
|
|
<button class="preview-nav preview-next" @onclick="() => NavigatePreview(1)" @onclick:stopPropagation="true">›</button>
|
|
}
|
|
|
|
@if (selectedAsset.HasPreview) {
|
|
<img class="preview-img" src="@PreviewUrl(selectedAsset.Id)" alt=""
|
|
onerror="this.onerror=null;this.src='@ThumbUrl(selectedAsset.Id)'" />
|
|
} else {
|
|
<img class="preview-img" src="@ThumbUrl(selectedAsset.Id)" alt="" />
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@code {
|
|
List<List<AssetPreviewDto>> allAssetPages = [];
|
|
List<AssetPreviewDto> flatList = [];
|
|
int currentPage = 1;
|
|
bool hasMore = true;
|
|
bool isLoading = true;
|
|
bool isLoadingMore;
|
|
AssetPreviewDto? selectedAsset;
|
|
int selectedIndex = -1;
|
|
ElementReference sentinelRef;
|
|
DotNetObjectReference<Home>? 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();
|
|
}
|
|
}
|
|
}
|