feat(ui): unassigned album selector in PersonForm, Add Albums button in CosplayerDetail
This commit is contained in:
@@ -99,6 +99,16 @@ public class AlbumController(
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all albums that have no person assigned.
|
||||
/// </summary>
|
||||
/// <returns>A list of unassigned album previews.</returns>
|
||||
[HttpGet("unassigned")]
|
||||
public ActionResult<List<AlbumPreviewDto>> GetUnassigned() {
|
||||
var albums = albumRepository.FindUnassigned().ToList();
|
||||
return Ok(albums.Select(x => x.ToAlbumPreviewDto()).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates an existing album.
|
||||
/// </summary>
|
||||
|
||||
@@ -60,6 +60,10 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
|
||||
/// <inheritdoc />
|
||||
public void Remove(Album album) => context.Albums.Remove(album);
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<Album> FindUnassigned() =>
|
||||
context.Albums.Where(a => a.PersonOwnerId == null);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => context.Dispose();
|
||||
}
|
||||
|
||||
@@ -79,4 +79,11 @@ public interface IAlbumRepository : IDisposable {
|
||||
/// <param name="pageSize">The number of results per page (default is 150).</param>
|
||||
/// <returns>A list of album IDs that match the search query.</returns>
|
||||
public IEnumerable<Album> SearchQuery(string query, int page = 0, int pageSize = 150);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds albums that have no person assigned.
|
||||
/// </summary>
|
||||
/// <returns>A collection of albums without a person owner.</returns>
|
||||
public IEnumerable<Album> FindUnassigned();
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/cosplayer/{Id:guid}"
|
||||
@using Butter.Dtos.Album
|
||||
@using Butter.Dtos.Person
|
||||
@using Butter.Types
|
||||
@using Microsoft.Extensions.Options
|
||||
@@ -6,6 +7,7 @@
|
||||
@using MilkStream.Client.Services
|
||||
|
||||
@inject PersonService personService
|
||||
@inject AlbumService albumService
|
||||
@inject LoginService loginService
|
||||
@inject IOptions<ServiceOptions> serviceOptions
|
||||
@inject NavigationManager navigationManager
|
||||
@@ -75,6 +77,9 @@
|
||||
</button>
|
||||
</div>
|
||||
@if (IsAdminOrCurator) {
|
||||
<button class="btn btn-outline-success btn-sm" @onclick="OpenAlbumAssigner">
|
||||
<i class="bi bi-plus-circle"></i> Add Albums
|
||||
</button>
|
||||
<button class="btn btn-outline-primary btn-sm" @onclick="OpenEditForm">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</button>
|
||||
@@ -106,6 +111,45 @@
|
||||
OnClosed="CloseForm" />
|
||||
}
|
||||
|
||||
@if (showAlbumAssigner && person != null) {
|
||||
<div class="modal-overlay" @onclick="CloseAlbumAssigner">
|
||||
<div class="modal-content" @onclick:stopPropagation="true">
|
||||
<div class="modal-header">
|
||||
<h5>Assign Albums to @person.Name</h5>
|
||||
<button class="btn-close" @onclick="CloseAlbumAssigner"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@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>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public Guid Id { get; set; }
|
||||
@@ -117,6 +161,9 @@
|
||||
string apiBase => serviceOptions.Value.BaseUrl.TrimEnd('/');
|
||||
bool IsAdminOrCurator => loginService.LoggedUser?.AccessLevel is EAccessLevel.Admin or EAccessLevel.Curator;
|
||||
string viewMode = "masonry";
|
||||
bool showAlbumAssigner;
|
||||
List<AlbumPreviewDto>? unassignedAlbums;
|
||||
HashSet<Guid> assignAlbumIds = [];
|
||||
|
||||
string TokenParam => string.IsNullOrEmpty(token) ? "" : $"?token={token}";
|
||||
|
||||
@@ -166,6 +213,32 @@
|
||||
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);
|
||||
}
|
||||
|
||||
async Task AssignSelectedAlbums() {
|
||||
if (person == null) return;
|
||||
foreach (var albumId in assignAlbumIds) {
|
||||
await albumService.AssignPersonAsync(albumId, person.Id);
|
||||
}
|
||||
showAlbumAssigner = false;
|
||||
unassignedAlbums = null;
|
||||
await LoadPerson();
|
||||
}
|
||||
|
||||
async Task ConfirmDelete() {
|
||||
if (person == null) return;
|
||||
await personService.DeletePersonAsync(person.Id);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
@using Butter.Dtos.Person
|
||||
@using Butter.Dtos.Album
|
||||
@using MilkStream.Client.Services
|
||||
|
||||
@inject PersonService personService
|
||||
@inject AlbumService albumService
|
||||
|
||||
@if (Show) {
|
||||
<div class="modal-overlay" @onclick="OnClose">
|
||||
@@ -35,6 +37,30 @@
|
||||
<div class="mt-2 text-muted small">Crop: @(cropX?.ToString("F1") ?? "auto") × @(cropY?.ToString("F1") ?? "auto")</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Assign Albums</label>
|
||||
@if (unassignedAlbums == null) {
|
||||
<div class="text-muted small">Loading...</div>
|
||||
} else if (unassignedAlbums.Count == 0) {
|
||||
<div class="text-muted small">All albums are already assigned.</div>
|
||||
} else {
|
||||
<div class="border rounded p-2" style="max-height: 200px; overflow-y: auto;">
|
||||
@foreach (var album in unassignedAlbums) {
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
id="@("album-" + album.Id)"
|
||||
checked="@selectedAlbumIds.Contains(album.Id)"
|
||||
@onchange="(e) => ToggleAlbum(album.Id, (bool)(e.Value ?? false))" />
|
||||
<label class="form-check-label" for="@("album-" + album.Id)">
|
||||
<span class="album-select-name">@album.Name</span>
|
||||
<span class="album-select-count text-muted small ms-1">@album.AssetCount items</span>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" @onclick="OnClose">Cancel</button>
|
||||
@@ -95,8 +121,11 @@
|
||||
float? cropZoom;
|
||||
bool showAssetPicker;
|
||||
bool showCropper;
|
||||
List<AlbumPreviewDto>? unassignedAlbums;
|
||||
HashSet<Guid> selectedAlbumIds = [];
|
||||
Guid? createdPersonId;
|
||||
|
||||
protected override void OnParametersSet() {
|
||||
protected override async Task OnParametersSetAsync() {
|
||||
if (Show && EditPerson != null) {
|
||||
name = EditPerson.Name;
|
||||
profileAssetId = EditPerson.ProfileAssetId;
|
||||
@@ -110,6 +139,14 @@
|
||||
cropY = null;
|
||||
cropZoom = null;
|
||||
}
|
||||
if (Show && unassignedAlbums == null) {
|
||||
unassignedAlbums = await albumService.GetUnassignedAsync();
|
||||
}
|
||||
if (!Show) {
|
||||
unassignedAlbums = null;
|
||||
selectedAlbumIds.Clear();
|
||||
createdPersonId = null;
|
||||
}
|
||||
}
|
||||
|
||||
void OpenAssetPicker() {
|
||||
@@ -142,7 +179,14 @@
|
||||
showCropper = false;
|
||||
}
|
||||
|
||||
void ToggleAlbum(Guid id, bool selected) {
|
||||
if (selected) selectedAlbumIds.Add(id);
|
||||
else selectedAlbumIds.Remove(id);
|
||||
}
|
||||
|
||||
async Task OnSave() {
|
||||
Guid? personId = EditPerson?.Id;
|
||||
|
||||
if (EditPerson == null) {
|
||||
var dto = new PersonCreateDto {
|
||||
Name = name,
|
||||
@@ -151,7 +195,8 @@
|
||||
ProfileCropY = cropY,
|
||||
ProfileCropZoom = cropZoom
|
||||
};
|
||||
await personService.CreatePersonAsync(dto);
|
||||
personId = await personService.CreatePersonAsync(dto);
|
||||
createdPersonId = personId;
|
||||
} else {
|
||||
var dto = new PersonUpdateDto {
|
||||
Name = name,
|
||||
@@ -163,6 +208,12 @@
|
||||
await personService.UpdatePersonAsync(EditPerson.Id, dto);
|
||||
}
|
||||
|
||||
if (personId.HasValue && selectedAlbumIds.Count > 0) {
|
||||
foreach (var albumId in selectedAlbumIds) {
|
||||
await albumService.AssignPersonAsync(albumId, personId.Value);
|
||||
}
|
||||
}
|
||||
|
||||
await OnSaved.InvokeAsync();
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,30 @@ public sealed class AlbumService(
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all albums that have no person assigned.
|
||||
/// </summary>
|
||||
/// <returns>A list of unassigned album previews, or null if the request failed.</returns>
|
||||
public async Task<List<AlbumPreviewDto>?> GetUnassignedAsync() {
|
||||
var response = await SendWithRefreshAsync(() => Client.GetAsync("/api/album/unassigned"));
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return await response.Content.ReadFromJsonAsync<List<AlbumPreviewDto>>();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Assigns a person to an album.
|
||||
/// </summary>
|
||||
/// <param name="albumId">The album ID.</param>
|
||||
/// <param name="personId">The person ID to assign.</param>
|
||||
public async Task AssignPersonAsync(Guid albumId, Guid personId) {
|
||||
var dto = new AlbumUpdateDto { Person = personId };
|
||||
var response = await Client.PostAsJsonAsync($"/api/album/{albumId}", dto);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new album.
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user