Merge branch 'develop' into feature/cascade-visibility-skip-deleted

This commit is contained in:
2026-08-17 21:17:31 +02:00
18 changed files with 68 additions and 264 deletions
@@ -1,37 +0,0 @@
@* Generic collapsible section with clickable header and chevron toggle *@
<div class="@Class">
<div class="d-flex align-items-center gap-2 cursor-pointer" @onclick="Toggle" role="button">
@if (TitleContent is not null) {
@TitleContent
} else if (!string.IsNullOrEmpty(Title)) {
<span>@Title</span>
}
<span class="ms-auto small @(Expanded ? "bi-chevron-up" : "bi-chevron-down")"></span>
</div>
@if (Expanded) {
<div class="@ContentClass">
@ChildContent
</div>
}
</div>
@code {
[Parameter] public string? Title { get; set; }
[Parameter] public RenderFragment? TitleContent { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
[Parameter] public bool InitiallyExpanded { get; set; }
[Parameter] public string? Class { get; set; }
[Parameter] public string? ContentClass { get; set; }
[Parameter] public EventCallback<bool> ExpandedChanged { get; set; }
bool Expanded { get; set; }
protected override void OnInitialized() {
Expanded = InitiallyExpanded;
}
async Task Toggle() {
Expanded = !Expanded;
await ExpandedChanged.InvokeAsync(Expanded);
}
}
@@ -113,8 +113,6 @@
[Parameter]
public FolderFullDto Folder { get; set; } = new FolderFullDto();
[Parameter]
public EventCallback<Guid> OnDeleted { get; set; }
[Parameter]
public EventCallback<Guid> OnDeleteRequested { get; set; }
@@ -1,19 +0,0 @@
@* Section wrapper for a group of jobs with title and count *@
<div class="mb-3">
<div class="d-flex align-items-center mb-1">
<h6 class="mb-0 text-muted text-uppercase small">@Title @(Count is not null ? $"({Count})" : "")</h6>
@if (Extra is not null) {
<span class="ms-auto">@Extra</span>
}
</div>
<div class="border rounded">
@ChildContent
</div>
</div>
@code {
[Parameter] public required string Title { get; set; }
[Parameter] public int? Count { get; set; }
[Parameter] public RenderFragment? Extra { get; set; }
[Parameter] public required RenderFragment ChildContent { get; set; }
}
@@ -2,14 +2,13 @@
@using Butter.Dtos.Album
@using Butter.Dtos.Asset
@using Butter.Types
@using Microsoft.Extensions.Options
@implements IAsyncDisposable
@inject AlbumService albumService
@inject AssetService assetService
@inject LoginService loginService
@inject IJSRuntime jsRuntime
@inject IOptions<ServiceOptions> serviceOptions
@inject MediaService mediaService
@inject NavigationManager navigationManager
<PageTitle>@(album?.Name ?? "Album")</PageTitle>
@@ -157,7 +156,7 @@
style="aspect-ratio:@GetAspectRatio(preview)"
@onclick="() => HandleTileClick(index, assetId)">
@if (preview == null || preview.HasThumbnail) {
<img src="@ThumbUrl(assetId)"
<img src="@mediaService.ThumbUrl(assetId)"
loading="lazy" alt=""
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
<div class="tile-fallback" style="display:none">
@@ -249,10 +248,6 @@
HashSet<Guid> selectedImageIds = [];
Guid? selectedAssetId;
int selectedIndex = -1;
string token = string.Empty;
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
string viewMode = "masonry";
bool _masonrySetup;
@@ -293,7 +288,6 @@
protected override async Task OnParametersSetAsync() {
await jsRuntime.InvokeVoidAsync("masonryObserver.unlockBodyScroll");
token = loginService.AuthInfo?.Token ?? string.Empty;
await LoadAlbum();
}
@@ -309,9 +303,6 @@
protected override void OnInitialized() {
loginService.LoggedUserChanged += (_, _) => StateHasChanged();
loginService.AuthInfoChanged += (_, info) => {
token = info?.Token ?? string.Empty;
};
}
async Task LoadAlbum() {
@@ -504,8 +495,6 @@
return "4 / 3";
}
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
public async ValueTask DisposeAsync()
{
await jsRuntime.InvokeVoidAsync("masonryLayout.teardown", ".album-assets-grid.masonry");
@@ -3,13 +3,12 @@
@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 MediaService mediaService
@inject NavigationManager navigationManager
<PageTitle>@(person?.Name ?? "Cosplayer")</PageTitle>
@@ -40,7 +39,7 @@
var cy = person.ProfileCropY ?? 50;
var z = person.ProfileCropZoom ?? 0.6f;
<div class="cosplayer-avatar-clip">
<img src="@ThumbUrl(person.ProfileAssetId.Value)" alt=""
<img src="@mediaService.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>
@@ -317,8 +316,6 @@
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;
@@ -341,8 +338,6 @@
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)
@@ -350,15 +345,11 @@
.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() {
@@ -372,7 +363,7 @@
.Where(a => a.CoverAssetId.HasValue)
.OrderBy(_ => Random.Shared.Next())
.Take(3)
.Select(a => ThumbUrl(a.CoverAssetId!.Value))
.Select(a => mediaService.ThumbUrl(a.CoverAssetId!.Value))
.ToList();
}
@@ -522,6 +513,4 @@
if (_pendingDeleteAction != null)
await _pendingDeleteAction();
}
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
}
+4 -15
View File
@@ -1,13 +1,12 @@
@page "/"
@using Butter.Dtos.Asset
@using Butter.Types
@using Microsoft.Extensions.Options
@implements IAsyncDisposable
@inject AssetService assetService
@inject LoginService loginService
@inject IJSRuntime jsRuntime
@inject IOptions<ServiceOptions> serviceOptions
@inject IJSRuntime jsRuntime
@inject MediaService mediaService
<PageTitle>Home</PageTitle>
@@ -36,7 +35,7 @@
style="aspect-ratio:@GetAspectRatio(asset)"
@onclick="() => OpenPreview(asset, capturedIndex)">
@if (asset.HasThumbnail) {
<img src="@ThumbUrl(asset.Id)"
<img src="@mediaService.ThumbUrl(asset.Id)"
loading="lazy" alt=""
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
<div class="tile-fallback" style="display:none">
@@ -136,18 +135,10 @@
ElementReference topSentinelRef;
DotNetObjectReference<Home>? dotNetRef;
Guid _randomSeed;
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;
loginService.AuthInfoChanged += (_, _) => StateHasChanged();
await LoadFirstPage();
}
@@ -277,8 +268,6 @@
return "4 / 3";
}
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
public async ValueTask DisposeAsync() {
await jsRuntime.InvokeVoidAsync("masonryObserver.unlockBodyScroll");
if (dotNetRef != null) {
@@ -10,16 +10,13 @@
</div>
} else {
<div class="progress" role="progressbar" style="height: @Height">
<div class="progress-bar @BgClass" style="width: @((Value * 100).ToString(CultureInfo.InvariantCulture))%">
@if (ShowPercent) { @($"{Value * 100:F0}%") }
</div>
<div class="progress-bar @BgClass" style="width: @((Value * 100).ToString(CultureInfo.InvariantCulture))%"></div>
</div>
}
@code {
[Parameter] public float Value { get; set; }
[Parameter] public string? Color { get; set; }
[Parameter] public bool ShowPercent { get; set; }
[Parameter] public string Size { get; set; } = "sm";
[Parameter] public IReadOnlyList<(float Width, string Color)>? Segments { get; set; }
@@ -124,13 +124,6 @@ public static class RegexHighlighter
return new MarkupString(sb.ToString());
}
/// <summary>
/// Returns the set of CSS class names used by the highlighter for embedding in isolated CSS.
/// </summary>
public static string CssClasses => string.Join(" ",
ClassAnchor, ClassQuantifier, ClassCharClass, ClassEscape,
ClassNamedGroup, ClassGroupRef, ClassOperator, ClassLiteral);
private static int FindNamedGroupEnd(ReadOnlySpan<char> span, int start) =>
FindMatchingParen(span, start);
@@ -2,14 +2,13 @@
@using Butter.Dtos.Asset
@using Butter.Dtos.Person
@using Butter.Types
@using Microsoft.Extensions.Options
@implements IDisposable
@inject AssetService assetService
@inject AlbumService albumService
@inject PersonService personService
@inject LoginService loginService
@inject IOptions<ServiceOptions> serviceOptions
@inject LoginService loginService
@inject MediaService mediaService
@if (Embedded) {
<div class="picker-embedded">
@@ -86,7 +85,7 @@
<div class="picker-tile @(selectedIds.Contains(asset.Id) ? "selected" : "")"
@key="asset.Id" @onclick="() => ToggleSelection(asset.Id)">
@if (asset.HasThumbnail) {
<img src="@ThumbUrl(asset.Id)" loading="lazy" alt=""
<img src="@mediaService.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 {
@@ -137,7 +136,7 @@
<div class="picker-tile @(selectedIds.Contains(asset.Id) ? "selected" : "")"
@key="asset.Id" @onclick="() => ToggleSelection(asset.Id)">
@if (asset.HasThumbnail) {
<img src="@ThumbUrl(asset.Id)" loading="lazy" alt=""
<img src="@mediaService.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 {
@@ -254,7 +253,7 @@
<div class="picker-tile @(selectedIds.Contains(asset.Id) ? "selected" : "")"
@key="asset.Id" @onclick="() => ToggleSelection(asset.Id)">
@if (asset.HasThumbnail) {
<img src="@ThumbUrl(asset.Id)" loading="lazy" alt=""
<img src="@mediaService.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 {
@@ -305,7 +304,7 @@
<div class="picker-tile @(selectedIds.Contains(asset.Id) ? "selected" : "")"
@key="asset.Id" @onclick="() => ToggleSelection(asset.Id)">
@if (asset.HasThumbnail) {
<img src="@ThumbUrl(asset.Id)" loading="lazy" alt=""
<img src="@mediaService.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 {
@@ -429,7 +428,6 @@
string currentPath = "";
HashSet<Guid> selectedIds = [];
string searchQuery = string.Empty;
string token = string.Empty;
int currentPage;
bool isLoading;
bool hasMore = true;
@@ -455,15 +453,12 @@
const int PageSize = 150;
bool isAdminOrCurator => loginService.IsAdminOrCurator;
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
protected override async Task OnInitializedAsync() {
if (Embedded) await InitializeAsync();
}
protected override async Task OnParametersSetAsync() {
token = loginService.AuthInfo?.Token ?? string.Empty;
if (!Embedded && Show && !_initializing) {
_initializing = true;
await InitializeAsync();
@@ -613,8 +608,6 @@
_personSearchTimer?.Dispose();
}
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
static string TruncateName(string name, int maxLen = 40) {
if (name.Length <= maxLen) return name;
var lastSlash = name.LastIndexOf('/');
@@ -1,15 +1,14 @@
@using Butter.Dtos.Album
@using Butter.Types
@using Microsoft.Extensions.Options
@inject IOptions<ServiceOptions> serviceOptions
@inject LoginService loginService
@inject LoginService loginService
@inject MediaService mediaService
<div class="@GetCardClass()" @key="Album.Id" style="@GetCardStyle()">
<a href="/albums/@Album.Id" class="album-card-overlay" aria-label="View album @Album.Name"
@onclick="HandleOverlayClick" @onclick:preventDefault="SelectionMode"></a>
@if (Album.CoverAssetId.HasValue) {
<img src="@ThumbUrl(Album.CoverAssetId.Value)"
<img src="@mediaService.ThumbUrl(Album.CoverAssetId.Value)"
loading="lazy" alt=""
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
<div class="album-card-fallback" style="display:none">
@@ -54,12 +53,6 @@
[Parameter]
public string ViewMode { get; set; } = "masonry";
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
string token => loginService.AuthInfo?.Token ?? string.Empty;
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
string GetCardClass() {
var classes = new List<string> { "album-card" };
if (SelectionMode) {
@@ -1,9 +1,6 @@
@using Microsoft.Extensions.Options
@using MilkStream.Client.Services
@using System.Linq
@inject LoginService loginService
@inject IOptions<ServiceOptions> serviceOptions
@inject MediaService mediaService
<ModalFrame Title="Select Cover Image" Show="Show" Size="lg" OnClose="OnCancel">
<Body>
@@ -15,7 +12,7 @@
<div class="picker-tile @(SelectedAssetId == id ? "selected" : "")"
@key="id"
@onclick="() => SelectAsset(id)">
<img src="@ThumbUrl(id)"
<img src="@mediaService.ThumbUrl(id)"
loading="lazy" alt=""
onerror="this.style.display='none';this.nextElementSibling.style.display='flex'" />
<div class="tile-fallback" style="display:none">
@@ -47,15 +44,6 @@
[Parameter] public EventCallback<Guid?> OnSelected { get; set; }
[Parameter] public EventCallback OnClosed { get; set; }
string token = string.Empty;
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
protected override void OnParametersSet() {
token = loginService.AuthInfo?.Token ?? string.Empty;
}
void SelectAsset(Guid id) {
SelectedAssetId = id;
}
@@ -68,6 +56,4 @@
async Task OnCancel() {
await OnClosed.InvokeAsync();
}
string ThumbUrl(Guid id) => $"{apiBase}/api/media/thumb/{id}{TokenParam}";
}
@@ -1,12 +1,11 @@
@using Butter.Dtos.Person
@using Butter.Types
@using Microsoft.Extensions.Options
@using System.Globalization
@implements IAsyncDisposable
@inject PersonService personService
@inject LoginService loginService
@inject IOptions<ServiceOptions> serviceOptions
@inject MediaService mediaService
@inject IJSRuntime jsRuntime
@if (isLoading) {
@@ -34,7 +33,7 @@
var cy1 = person.ProfileCropY ?? 50;
var zoom1 = person.ProfileCropZoom ?? 0.6f;
<div class="cosplayer-card-img-wrap">
<img src="@ThumbUrl(person.ProfileAssetId.Value)" alt=""
<img src="@mediaService.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'" />
@@ -92,14 +91,6 @@
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
@@ -183,8 +174,6 @@
}
}
string ThumbUrl(Guid id) => $"{_apiBase}/api/media/thumb/{id}{TokenParam}";
string GetCardClass(PersonPreviewDto person) {
var classes = new List<string> { "cosplayer-card" };
if (SelectionMode) {
@@ -198,10 +187,6 @@
return string.Join(" ", classes);
}
void OnAuthInfoChanged(object? _, AuthInfo? info) {
_token = info?.Token ?? string.Empty;
}
async Task DisposeObserver() {
if (dotNetRef != null) {
await jsRuntime.InvokeVoidAsync("cosplayerObserver.dispose");
@@ -211,7 +196,6 @@
}
public async ValueTask DisposeAsync() {
loginService.AuthInfoChanged -= OnAuthInfoChanged;
await DisposeObserver();
}
@@ -1,8 +1,5 @@
@using Microsoft.Extensions.Options
@inject IOptions<ServiceOptions> Options
@inject LoginService LoginService
@inject IJSRuntime JSRuntime
@inject IJSRuntime JSRuntime
@inject MediaService mediaService
@if (Show && AssetId != Guid.Empty) {
<div class="preview-overlay" @onkeydown="OnKeyDown" tabindex="0" @onclick="OnCloseCallback">
@@ -18,10 +15,10 @@
<div class="preview-image-wrapper">
@if (HasPreview) {
<img class="preview-img" src="@PreviewUrl" alt=""
onerror="this.onerror=null;this.src='@ThumbUrl'" />
<img class="preview-img" src="@mediaService.PreviewUrl(AssetId)" alt=""
onerror="this.onerror=null;this.src='@mediaService.ThumbUrl(AssetId)'" />
} else {
<img class="preview-img" src="@ThumbUrl" alt="" />
<img class="preview-img" src="@mediaService.ThumbUrl(AssetId)" alt="" />
}
<div class="preview-info">
<div class="preview-info-text">
@@ -32,7 +29,7 @@
<span class="preview-info-filename">@Filename</span>
}
<div class="preview-info-actions">
<a class="preview-action-btn" href="@OriginalUrl" target="_blank"
<a class="preview-action-btn" href="@mediaService.OriginalUrl(AssetId)" target="_blank"
title="View full size" @onclick:stopPropagation="true">
<i class="bi bi-arrows-fullscreen"></i>
</a>
@@ -59,18 +56,6 @@
[Parameter] public EventCallback OnClose { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
string token = string.Empty;
string apiBase => Options.Value.BaseUrl.TrimEnd('/');
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
string ThumbUrl => $"{apiBase}/api/media/thumb/{AssetId}{TokenParam}";
string PreviewUrl => $"{apiBase}/api/media/preview/{AssetId}{TokenParam}";
string OriginalUrl => $"{apiBase}/api/media/original/{AssetId}{TokenParam}";
protected override void OnInitialized() {
LoginService.AuthInfoChanged += (_, info) => { token = info?.Token ?? string.Empty; };
token = LoginService.AuthInfo?.Token ?? string.Empty;
}
async Task OnCloseCallback() {
await OnClose.InvokeAsync();
}
@@ -90,6 +75,6 @@
}
async Task Download() {
await JSRuntime.InvokeVoidAsync("masonryObserver.downloadFile", OriginalUrl);
await JSRuntime.InvokeVoidAsync("masonryObserver.downloadFile", mediaService.OriginalUrl(AssetId));
}
}
@@ -1,7 +1,5 @@
@using Microsoft.Extensions.Options
@inject IJSRuntime jsRuntime
@inject IOptions<ServiceOptions> serviceOptions
@inject LoginService loginService
@inject IJSRuntime jsRuntime
@inject MediaService mediaService
@if (Show && AssetId.HasValue) {
<div class="modal-overlay" style="background: rgba(0, 0, 0, 0.85)" @onclick="OnCancel">
@@ -72,11 +70,6 @@
/// </summary>
[Parameter] public EventCallback OnClosed { get; set; }
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
string token => loginService.AuthInfo?.Token ?? string.Empty;
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
string ImageUrl => $"{apiBase}/api/media/thumb/{AssetId!.Value}{TokenParam}";
protected override void OnInitialized() {
_instanceId = ++_counter;
}
@@ -93,7 +86,7 @@
if (_pendingInit) {
_pendingInit = false;
await jsRuntime.InvokeVoidAsync("profileCropper.initialize",
ContainerId, ImageUrl, CropX ?? 50, CropY ?? 50, CropZoom ?? 0.6f);
ContainerId, mediaService.ThumbUrl(AssetId!.Value), CropX ?? 50, CropY ?? 50, CropZoom ?? 0.6f);
}
}
@@ -1,15 +1,13 @@
@using Butter.Dtos.Album
@using Butter.Dtos.Person
@using Microsoft.Extensions.Options
@using MilkStream.Client.Services
@namespace MilkStream.Client.Components.Shared
@inject PersonService PersonSvc
@inject AlbumService AlbumSvc
@inject NavigationManager Nav
@inject IOptions<ServiceOptions> ServiceOptions
@inject LoginService LoginSvc
@inject PersonService PersonSvc
@inject AlbumService AlbumSvc
@inject NavigationManager Nav
@inject MediaService mediaService
<div class="search-dropdown-container position-relative">
<div class="d-flex">
@@ -67,7 +65,7 @@
@onmouseenter="() => OnItemHover(idx)">
@if (item.ImageId.HasValue)
{
<img class="search-dropdown-thumb" src="@ThumbUrl(item.ImageId.Value)" alt="" loading="lazy" />
<img class="search-dropdown-thumb" src="@mediaService.ThumbUrl(item.ImageId.Value)" alt="" loading="lazy" />
}
else
{
@@ -85,7 +83,7 @@
@onmouseenter="() => OnItemHover(idx)">
@if (item.ImageId.HasValue)
{
<img class="search-dropdown-thumb-rounded" src="@ThumbUrl(item.ImageId.Value)" alt="" loading="lazy" />
<img class="search-dropdown-thumb-rounded" src="@mediaService.ThumbUrl(item.ImageId.Value)" alt="" loading="lazy" />
}
else
{
@@ -117,12 +115,6 @@
int _selectedIndex = -1;
CancellationTokenSource? _blurCts;
string ApiBase => ServiceOptions.Value.BaseUrl.TrimEnd('/');
string Token => LoginSvc.AuthInfo?.Token ?? string.Empty;
string TokenParam => string.IsNullOrEmpty(Token) ? "" : $"?token={Token}";
string ThumbUrl(Guid id) => $"{ApiBase}/api/media/thumb/{id}{TokenParam}";
async Task OnInput(ChangeEventArgs e)
{
_query = e.Value?.ToString() ?? string.Empty;
@@ -12,15 +12,6 @@ public sealed class FoldersService(
LoginService loginService,
ILogger<FoldersService> logger
) : AuthServiceBase(options, httpClientFactory, loginService, logger) {
/// <summary>
/// Fired when folders are saved.
/// </summary>
public event EventHandler? SaveFolders;
/// <summary>
/// Invokes the <see cref="SaveFolders"/> event.
/// </summary>
public void OnSaveFolders() => SaveFolders?.Invoke(this, EventArgs.Empty);
/// <summary>
/// Gets all tracked folders.
/// </summary>
@@ -33,19 +24,6 @@ public sealed class FoldersService(
return null;
}
/// <summary>
/// Gets a folder by its ID.
/// </summary>
/// <param name="id">The folder ID.</param>
/// <returns>The folder details, or null if not found.</returns>
public async Task<FolderFullDto?> GetFolderById(Guid id) {
var response = await Client.GetAsync($"/api/folder/{id}");
if (response.IsSuccessStatusCode) return await response.Content.ReadFromJsonAsync<FolderFullDto>();
return null;
}
/// <summary>
/// Creates a new folder.
/// </summary>
+29 -20
View File
@@ -3,28 +3,37 @@ using Microsoft.Extensions.Options;
namespace MilkStream.Client.Services;
/// <summary>
/// Provides access to media files (original, thumbnails, previews) via the API.
/// Builds media URLs (original, thumbnail, preview) authenticated via a token query parameter.
/// Media is served by <c>MediaController</c> through <c>&lt;img&gt;</c>-accessible URLs, so the
/// JWT is passed as a <c>token</c> query parameter rather than an Authorization header.
/// </summary>
public sealed class MediaService : AuthServiceBase {
/// <inheritdoc />
protected override HttpClient Client { get; init; }
/// <inheritdoc />
protected override ILogger Logger { get; init; }
/// <param name="options">Service configuration options.</param>
/// <param name="loginService">The login service providing the current auth token.</param>
public sealed class MediaService(IOptions<ServiceOptions> options, LoginService loginService) {
string BaseUrl => options.Value.BaseUrl.TrimEnd('/');
string TokenParam {
get {
var token = loginService.AuthInfo?.Token;
return string.IsNullOrEmpty(token) ? "" : $"?token={token}";
}
}
/// <summary>
/// Initialises a new instance of the <see cref="MediaService"/> class.
/// Gets the thumbnail URL for an asset.
/// </summary>
/// <param name="options">Service configuration options.</param>
/// <param name="httpClientFactory">Factory for creating HTTP clients.</param>
/// <param name="loginService">The login service for authentication.</param>
/// <param name="logger">Logger instance.</param>
public MediaService(
IOptions<ServiceOptions> options,
IHttpClientFactory httpClientFactory,
LoginService loginService,
ILogger<MediaService> logger
) : base(options, httpClientFactory, loginService, logger) {
Client = base.Client;
Logger = logger;
}
/// <param name="id">The asset ID.</param>
public string ThumbUrl(Guid id) => $"{BaseUrl}/api/media/thumb/{id}{TokenParam}";
/// <summary>
/// Gets the preview URL for an asset.
/// </summary>
/// <param name="id">The asset ID.</param>
public string PreviewUrl(Guid id) => $"{BaseUrl}/api/media/preview/{id}{TokenParam}";
/// <summary>
/// Gets the original image URL for an asset.
/// </summary>
/// <param name="id">The asset ID.</param>
public string OriginalUrl(Guid id) => $"{BaseUrl}/api/media/original/{id}{TokenParam}";
}
-8
View File
@@ -1,8 +0,0 @@
namespace MilkStream.Client;
/// <summary>
/// Provides extension methods for string operations.
/// </summary>
public class StringExtensions {
}