3 Commits
12 changed files with 116 additions and 52 deletions
+4 -16
View File
@@ -137,22 +137,10 @@ public class PersonController(
var visibility = personBulkDto.Data.Visibility;
if (cascade != null && visibility.HasValue) {
var albums = cascade.ToAlbums || cascade.ToAssets
? albumRepository.FindByPersonIds(personBulkDto.Ids).ToList()
: [];
if (cascade.ToAlbums) {
foreach (var album in albums) {
album.Visibility = visibility.Value;
album.UpdatedAt = DateTime.UtcNow;
}
result.AlbumsUpdated = albums.Count;
}
if (cascade.ToAssets && albums.Count > 0) {
result.AssetsUpdated = assetRepository.BulkSetVisibilityByAlbumIds(
albums.Select(a => a.Id).ToList(), visibility.Value, uid, accessLevel);
}
if (cascade.ToAlbums)
result.AlbumsUpdated = albumRepository.BulkSetVisibilityByPersonIds(personBulkDto.Ids, visibility.Value);
if (cascade.ToAssets)
result.AssetsUpdated = assetRepository.BulkSetVisibilityByPersonIds(personBulkDto.Ids, visibility.Value);
}
personRepository.Save();
+8
View File
@@ -193,6 +193,14 @@ public class AlbumRepository(LactoseDbContext context) : IAlbumRepository {
public IEnumerable<Album> FindByPersonIds(IEnumerable<Guid> personIds) =>
context.Albums.Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value));
/// <inheritdoc />
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility) =>
context.Albums
.Where(a => a.PersonOwnerId != null && personIds.Contains(a.PersonOwnerId.Value))
.ExecuteUpdate(s => s
.SetProperty(a => a.Visibility, visibility)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow));
/// <inheritdoc />
public IEnumerable<Album> FindBulk(IEnumerable<Guid> ids) => context.Albums.Where(a => ids.Contains(a.Id));
+9
View File
@@ -114,6 +114,15 @@ public class AssetRepository(LactoseDbContext context, ILogger<AssetRepository>
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow));
}
/// <inheritdoc />
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility) =>
context.Assets
.Where(a => a.DeletedAt == null)
.Where(a => a.Albums!.Any(al => al.PersonOwnerId != null && personIds.Contains(al.PersonOwnerId.Value)))
.ExecuteUpdate(s => s
.SetProperty(a => a.Visibility, visibility)
.SetProperty(a => a.UpdatedAt, DateTime.UtcNow));
/// <inheritdoc />
public IEnumerable<Asset> FindBulk(IEnumerable<Guid> ids) => context.Assets
.AsSplitQuery()
+8
View File
@@ -69,6 +69,14 @@ public interface IAlbumRepository : IDisposable {
/// <returns>A collection of albums associated with the specified people.</returns>
public IEnumerable<Album> FindByPersonIds(IEnumerable<Guid> personIds);
/// <summary>
/// Bulk-updates the visibility of all albums owned by the specified people via a single SQL UPDATE.
/// </summary>
/// <param name="personIds">The IDs of the people.</param>
/// <param name="visibility">The target visibility level.</param>
/// <returns>The number of albums updated.</returns>
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility);
/// <summary>
/// Finds multiple albums by their IDs.
/// </summary>
+9
View File
@@ -57,6 +57,15 @@ public interface IAssetRepository : IDisposable {
/// <returns>The number of assets updated.</returns>
public int BulkSetVisibilityByAlbumIds(IEnumerable<Guid> albumIds, EVisibility visibility, Guid? userId, EAccessLevel accessLevel);
/// <summary>
/// Bulk-updates the visibility of non-deleted assets in any album owned by the specified people
/// via a single SQL UPDATE.
/// </summary>
/// <param name="personIds">The IDs of the people whose album assets to update.</param>
/// <param name="visibility">The target visibility level.</param>
/// <returns>The number of assets updated.</returns>
public int BulkSetVisibilityByPersonIds(IEnumerable<Guid> personIds, EVisibility visibility);
/// <summary>
/// Finds all assets created or updated between the given dates.
/// </summary>
+18 -10
View File
@@ -129,6 +129,7 @@
CascadeLabel="Also change visibility of all assets in these albums (skips deleted)"
CascadeEnabled="@cascadeToAssets"
CascadeEnabledChanged="@((bool v) => cascadeToAssets = v)"
Error="@visibilityError"
OnApply="ApplyAlbumVisibility"
OnClose="CloseVisibilityModal" />
}
@@ -187,6 +188,7 @@
List<MergeModal.MergeItem> mergeItems = [];
bool cascadeToAssets;
EVisibility selectedAlbumVisibility = EVisibility.Public;
string? visibilityError;
HashSet<Guid> selectedAlbumIds = [];
bool albumsAllSelected => albumGridRef != null && selectedAlbumIds.Count == albumGridRef.GetCurrentAlbumIds().Count && selectedAlbumIds.Count > 0;
string? searchQuery;
@@ -263,6 +265,7 @@
void OpenVisibilityModal() {
showVisibilityModal = true;
selectedAlbumVisibility = EVisibility.Public;
visibilityError = null;
}
void CloseVisibilityModal() {
@@ -304,16 +307,21 @@
async Task ApplyAlbumVisibility() {
if (selectedAlbumIds.Count == 0) return;
await albumService.BulkUpdateAlbumsAsync(
[.. selectedAlbumIds],
new AlbumUpdateDto { Visibility = selectedAlbumVisibility },
new CascadeOptions { ToAssets = cascadeToAssets }
);
showVisibilityModal = false;
cascadeToAssets = false;
selectMode = false;
selectedAlbumIds.Clear();
loadVersion++;
visibilityError = null;
try {
await albumService.BulkUpdateAlbumsAsync(
[.. selectedAlbumIds],
new AlbumUpdateDto { Visibility = selectedAlbumVisibility },
new CascadeOptions { ToAssets = cascadeToAssets }
);
showVisibilityModal = false;
cascadeToAssets = false;
selectMode = false;
selectedAlbumIds.Clear();
loadVersion++;
} catch (Exception) {
visibilityError = "The visibility update failed. Please try again.";
}
}
void OpenCreateForm() {
@@ -277,6 +277,7 @@
CascadeLabel="Also change visibility of all assets in these albums (skips deleted)"
CascadeEnabled="@cascadeToAssets"
CascadeEnabledChanged="@((bool v) => cascadeToAssets = v)"
Error="@visibilityError"
OnApply="ApplyAlbumVisibility"
OnClose="CloseVisibilityModal" />
}
@@ -314,6 +315,7 @@
Func<Task>? _pendingDeleteAction;
bool cascadeToAssets;
EVisibility selectedAlbumVisibility = EVisibility.Public;
string? visibilityError;
string viewMode = "masonry";
bool showAlbumAssigner;
List<AlbumPreviewDto>? unassignedAlbums;
@@ -475,6 +477,7 @@
void OpenVisibilityModal() {
showAlbumVisibilityModal = true;
selectedAlbumVisibility = EVisibility.Public;
visibilityError = null;
}
void CloseVisibilityModal() {
@@ -484,16 +487,21 @@
async Task ApplyAlbumVisibility() {
if (person == null || selectedAlbumIds.Count == 0) return;
await albumService.BulkUpdateAlbumsAsync(
[.. selectedAlbumIds],
new AlbumUpdateDto { Visibility = selectedAlbumVisibility },
new CascadeOptions { ToAssets = cascadeToAssets }
);
showAlbumVisibilityModal = false;
cascadeToAssets = false;
CancelSelectMode();
await LoadPerson();
albumLoadVersion++;
visibilityError = null;
try {
await albumService.BulkUpdateAlbumsAsync(
[.. selectedAlbumIds],
new AlbumUpdateDto { Visibility = selectedAlbumVisibility },
new CascadeOptions { ToAssets = cascadeToAssets }
);
showAlbumVisibilityModal = false;
cascadeToAssets = false;
CancelSelectMode();
await LoadPerson();
albumLoadVersion++;
} catch (Exception) {
visibilityError = "The visibility update failed. Please try again.";
}
}
async Task ConfirmDelete() {
@@ -127,6 +127,7 @@
CascadeLabel2="Change visibility of all assets in every album (skips deleted)"
CascadeEnabled2="@cascadeToAssets"
CascadeEnabled2Changed="@((bool v) => cascadeToAssets = v)"
Error="@visibilityError"
OnApply="ApplyVisibility"
OnClose="CloseVisibilityModal" />
}
@@ -159,6 +160,7 @@
bool cascadeToAlbums;
bool cascadeToAssets;
EVisibility selectedVisibility = EVisibility.Public;
string? visibilityError;
HashSet<Guid> selectedCosplayerIds = [];
string? searchQuery;
string? sortBy;
@@ -257,6 +259,7 @@
void OpenVisibilityModal() {
showVisibilityModal = true;
selectedVisibility = EVisibility.Public;
visibilityError = null;
cascadeToAlbums = false;
cascadeToAssets = false;
}
@@ -269,14 +272,19 @@
async Task ApplyVisibility() {
if (selectedCosplayerIds.Count == 0) return;
await personService.BulkUpdatePeopleAsync(
[.. selectedCosplayerIds],
new PersonUpdateDto { Visibility = selectedVisibility },
new CascadeOptions { ToAlbums = cascadeToAlbums, ToAssets = cascadeToAssets }
);
showVisibilityModal = false;
CancelSelectMode();
loadVersion++;
visibilityError = null;
try {
await personService.BulkUpdatePeopleAsync(
[.. selectedCosplayerIds],
new PersonUpdateDto { Visibility = selectedVisibility },
new CascadeOptions { ToAlbums = cascadeToAlbums, ToAssets = cascadeToAssets }
);
showVisibilityModal = false;
CancelSelectMode();
loadVersion++;
} catch (Exception) {
visibilityError = "The visibility update failed. Please try again.";
}
}
public void Dispose() {
@@ -363,6 +363,7 @@
ItemLabel="asset(s)"
Visibility="@_visibilitySelection"
VisibilityChanged="@((EVisibility v) => _visibilitySelection = v)"
Error="@_visibilityError"
OnApply="ApplyVisibility"
OnClose="() => _showVisibilityModal = false" />
}
@@ -441,6 +442,7 @@
private bool _showVisibilityModal;
private EVisibility _visibilitySelection = EVisibility.Public;
private string? _visibilityError;
private bool _showCreateAlbumModal;
private string _newAlbumName = "";
@@ -556,15 +558,25 @@
void OpenVisibilityModal() {
_visibilitySelection = EVisibility.Public;
_visibilityError = null;
_showVisibilityModal = true;
}
async Task ApplyVisibility() {
if (selectedIds.Count == 0) return;
await assetService.BulkUpdateAssetsAsync([.. selectedIds], new AssetUpdateDto { Visibility = _visibilitySelection });
_showVisibilityModal = false;
selectedIds.Clear();
await LoadContent();
_visibilityError = null;
try {
var ok = await assetService.BulkUpdateAssetsAsync([.. selectedIds], new AssetUpdateDto { Visibility = _visibilitySelection });
if (!ok) {
_visibilityError = "The visibility update failed. Please try again.";
return;
}
_showVisibilityModal = false;
selectedIds.Clear();
await LoadContent();
} catch (Exception) {
_visibilityError = "The visibility update failed. Please try again.";
}
}
void OpenCreateAlbumModal() {
@@ -39,6 +39,9 @@
</div>
}
}
@if (!string.IsNullOrEmpty(Error)) {
<div class="alert alert-danger py-1 mb-0">@Error</div>
}
</div>
</Body>
<Footer>
@@ -68,6 +71,7 @@
[Parameter] public string? CascadeLabel2 { get; set; }
[Parameter] public bool CascadeEnabled2 { get; set; }
[Parameter] public EventCallback<bool> CascadeEnabled2Changed { get; set; }
[Parameter] public string? Error { get; set; }
bool _isApplying;
+3 -2
View File
@@ -140,10 +140,11 @@ public sealed class AlbumService(
/// <param name="ids">The album IDs to update.</param>
/// <param name="dto">The shared update values.</param>
/// <param name="cascade">Optional cascade options (e.g. cascade a visibility change to assets).</param>
public async Task<bool> BulkUpdateAlbumsAsync(List<Guid> ids, AlbumUpdateDto dto, CascadeOptions? cascade = null) {
/// <exception cref="HttpRequestException">Thrown when the request fails.</exception>
public async Task BulkUpdateAlbumsAsync(List<Guid> ids, AlbumUpdateDto dto, CascadeOptions? cascade = null) {
var payload = new BulkDto<AlbumUpdateDto> { Ids = ids, Data = dto, Cascade = cascade };
var response = await Client.PostAsJsonAsync("/api/album", payload);
return response.IsSuccessStatusCode;
response.EnsureSuccessStatusCode();
}
/// <summary>
+3 -2
View File
@@ -116,10 +116,11 @@ public sealed class PersonService(
/// <param name="ids">The person IDs to update.</param>
/// <param name="dto">The shared update values.</param>
/// <param name="cascade">Optional cascade options (e.g. cascade a visibility change to albums and assets).</param>
public async Task<bool> BulkUpdatePeopleAsync(List<Guid> ids, PersonUpdateDto dto, CascadeOptions? cascade = null) {
/// <exception cref="HttpRequestException">Thrown when the request fails.</exception>
public async Task BulkUpdatePeopleAsync(List<Guid> ids, PersonUpdateDto dto, CascadeOptions? cascade = null) {
var payload = new BulkDto<PersonUpdateDto> { Ids = ids, Data = dto, Cascade = cascade };
var response = await Client.PostAsJsonAsync("/api/person", payload);
return response.IsSuccessStatusCode;
response.EnsureSuccessStatusCode();
}
/// <summary>