From ad152aa26aa1f9788bd6000a53b0ead19e945702 Mon Sep 17 00:00:00 2001 From: Joe Milazzo Date: Fri, 18 Apr 2025 05:48:37 -0600 Subject: [PATCH] v0.8.6.1 - A few small issues Hotfix (#3744) Co-authored-by: Amelia <77553571+Fesaa@users.noreply.github.com> Co-authored-by: Weblate (bot) Co-authored-by: Lyrq --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- API/Controllers/VolumeController.cs | 29 ++++++++++++++++- API/Data/Repositories/VolumeRepository.cs | 15 +++++++++ API/I18N/pl.json | 6 ++-- UI/Web/src/app/_services/action.service.ts | 13 +++++++- UI/Web/src/app/_services/volume.service.ts | 4 +++ .../manage-email-settings.component.ts | 12 +++++-- .../manage-media-settings.component.ts | 10 ++++-- .../manage-settings.component.ts | 21 ++++++++++-- .../manage-tasks-settings.component.ts | 21 ++++++++++-- .../volume-card/volume-card.component.html | 29 ++++++++--------- .../draggable-ordered-list.component.ts | 2 +- .../series-detail/series-detail.component.ts | 32 +++++++++++++++---- UI/Web/src/assets/langs/en.json | 1 + 14 files changed, 159 insertions(+), 38 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f52d914e5..a7eca3f96 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -28,7 +28,7 @@ body: label: Kavita Version Number - If you don't see your version number listed, please update Kavita and see if your issue still persists. multiple: false options: - - 0.8.6.0 - Stable + - 0.8.6.1 - Stable - Nightly Testing Branch validations: required: true diff --git a/API/Controllers/VolumeController.cs b/API/Controllers/VolumeController.cs index 7181f6eef..db1381d9d 100644 --- a/API/Controllers/VolumeController.cs +++ b/API/Controllers/VolumeController.cs @@ -1,4 +1,6 @@ -using System.Threading.Tasks; +using System.Linq; +using System.Threading.Tasks; +using API.Constants; using API.Data; using API.Data.Repositories; using API.DTOs; @@ -54,4 +56,29 @@ public class VolumeController : BaseApiController return Ok(false); } + + [Authorize(Policy = "RequireAdminRole")] + [HttpPost("multiple")] + public async Task> DeleteMultipleVolumes(int[] volumesIds) + { + var volumes = await _unitOfWork.VolumeRepository.GetVolumesById(volumesIds); + if (volumes.Count != volumesIds.Length) + { + return BadRequest(_localizationService.Translate(User.GetUserId(), "volume-doesnt-exist")); + } + + _unitOfWork.VolumeRepository.Remove(volumes); + + if (!await _unitOfWork.CommitAsync()) + { + return Ok(false); + } + + foreach (var volume in volumes) + { + await _eventHub.SendMessageAsync(MessageFactory.VolumeRemoved, MessageFactory.VolumeRemovedEvent(volume.Id, volume.SeriesId), false); + } + + return Ok(true); + } } diff --git a/API/Data/Repositories/VolumeRepository.cs b/API/Data/Repositories/VolumeRepository.cs index cb0783a89..4b07ade96 100644 --- a/API/Data/Repositories/VolumeRepository.cs +++ b/API/Data/Repositories/VolumeRepository.cs @@ -35,6 +35,7 @@ public interface IVolumeRepository void Add(Volume volume); void Update(Volume volume); void Remove(Volume volume); + void Remove(IList volumes); Task> GetFilesForVolume(int volumeId); Task GetVolumeCoverImageAsync(int volumeId); Task> GetChapterIdsByVolumeIds(IReadOnlyList volumeIds); @@ -43,6 +44,7 @@ public interface IVolumeRepository Task GetVolumeDtoAsync(int volumeId, int userId); Task> GetVolumesForSeriesAsync(IList seriesIds, bool includeChapters = false); Task> GetVolumes(int seriesId); + Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None); Task GetVolumeByIdAsync(int volumeId); Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat); Task> GetCoverImagesForLockedVolumesAsync(); @@ -72,6 +74,10 @@ public class VolumeRepository : IVolumeRepository { _context.Volume.Remove(volume); } + public void Remove(IList volumes) + { + _context.Volume.RemoveRange(volumes); + } /// /// Returns a list of non-tracked files for a given volume. @@ -180,6 +186,15 @@ public class VolumeRepository : IVolumeRepository .OrderBy(vol => vol.MinNumber) .ToListAsync(); } + public async Task> GetVolumesById(IList volumeIds, VolumeIncludes includes = VolumeIncludes.None) + { + return await _context.Volume + .Where(vol => volumeIds.Contains(vol.Id)) + .Includes(includes) + .AsSplitQuery() + .OrderBy(vol => vol.MinNumber) + .ToListAsync(); + } /// /// Returns a single volume with Chapter and Files diff --git a/API/I18N/pl.json b/API/I18N/pl.json index db3fa5063..68a4a1a4f 100644 --- a/API/I18N/pl.json +++ b/API/I18N/pl.json @@ -204,8 +204,8 @@ "person-image-doesnt-exist": "Osoba nie istnieje w CoversDB", "email-taken": "Adres e-mail jest już używany", "kavitaplus-restricted": "Jest to dostępne tylko dla Kavita+", - "smart-filter-name-required": "Strona internetowa", - "sidenav-stream-only-delete-smart-filter": "Jedynie filtry filtrowe mogą zostać usunięte z SideNav", - "dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów mogą zostać usunięte z rozdzielczości", + "smart-filter-name-required": "Inteligentny filtr wymaga nazwy", + "sidenav-stream-only-delete-smart-filter": "Tylko inteligentne filtry mogą zostać usunięte z panelu bocznego", + "dashboard-stream-only-delete-smart-filter": "Tylko inteligentne strumienie filtrów może zostać usunięte z głównego panelu", "smart-filter-system-name": "Nie można użyć nazwy systemu dostarczanego strumieniem" } diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts index 8bf6cdacd..1cf4e448e 100644 --- a/UI/Web/src/app/_services/action.service.ts +++ b/UI/Web/src/app/_services/action.service.ts @@ -472,8 +472,19 @@ export class ActionService { }); } + async deleteMultipleVolumes(volumes: Array, callback?: BooleanActionCallback) { + // TODO: Change translation key back to "toasts.confirm-delete-multiple-volumes" + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: volumes.length}))) return; + + this.volumeService.deleteMultipleVolumes(volumes.map(v => v.id)).subscribe((success) => { + if (callback) { + callback(success); + } + }) + } + async deleteMultipleChapters(seriesId: number, chapterIds: Array, callback?: BooleanActionCallback) { - if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters'))) return; + if (!await this.confirmService.confirm(translate('toasts.confirm-delete-multiple-chapters', {count: chapterIds.length}))) return; this.chapterService.deleteMultipleChapters(seriesId, chapterIds.map(c => c.id)).subscribe(() => { if (callback) { diff --git a/UI/Web/src/app/_services/volume.service.ts b/UI/Web/src/app/_services/volume.service.ts index 16857b3d2..f53a20543 100644 --- a/UI/Web/src/app/_services/volume.service.ts +++ b/UI/Web/src/app/_services/volume.service.ts @@ -21,6 +21,10 @@ export class VolumeService { return this.httpClient.delete(this.baseUrl + 'volume?volumeId=' + volumeId); } + deleteMultipleVolumes(volumeIds: number[]) { + return this.httpClient.post(this.baseUrl + "volume/multiple", volumeIds) + } + updateVolume(volume: any) { return this.httpClient.post(this.baseUrl + 'volume/update', volume, TextResonse); } diff --git a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts index d9aa1decb..8546ad921 100644 --- a/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts +++ b/UI/Web/src/app/admin/manage-email-settings/manage-email-settings.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from 'rxjs'; +import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from 'rxjs'; import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import {translate, TranslocoModule} from "@jsverse/transloco"; @@ -46,15 +46,21 @@ export class ManageEmailSettingsComponent implements OnInit { // Automatically save settings as we edit them this.settingsForm.valueChanges.pipe( - debounceTime(300), distinctUntilChanged(), + debounceTime(300), filter(_ => this.settingsForm.valid), takeUntilDestroyed(this.destroyRef), switchMap(_ => { const data = this.packData(); - return this.settingsService.updateServerSettings(data); + return this.settingsService.updateServerSettings(data).pipe(catchError(err => { + console.error(err); + return of(null); + })); }), tap(settings => { + if (!settings) { + return; + } this.serverSettings = settings; this.cdRef.markForCheck(); }) diff --git a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts index fbfb256dc..ac62e2038 100644 --- a/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts +++ b/UI/Web/src/app/admin/manage-media-settings/manage-media-settings.component.ts @@ -1,7 +1,7 @@ import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, inject, OnInit} from '@angular/core'; import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms'; import {ToastrService} from 'ngx-toastr'; -import {debounceTime, distinctUntilChanged, filter, switchMap, take, tap} from 'rxjs'; +import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, take, tap} from 'rxjs'; import {SettingsService} from '../settings.service'; import {ServerSettings} from '../_models/server-settings'; import {DirectoryPickerComponent, DirectoryPickerResult} from '../_modals/directory-picker/directory-picker.component'; @@ -55,9 +55,15 @@ export class ManageMediaSettingsComponent implements OnInit { takeUntilDestroyed(this.destroyRef), switchMap(_ => { const data = this.packData(); - return this.settingsService.updateServerSettings(data); + return this.settingsService.updateServerSettings(data).pipe(catchError(err => { + console.error(err); + return of(null); + })); }), tap(settings => { + if (!settings) { + return; + } const encodingChanged = this.serverSettings.encodeMediaAs !== settings.encodeMediaAs; if (encodingChanged) { diff --git a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts index 33941f768..7c669e651 100644 --- a/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts +++ b/UI/Web/src/app/admin/manage-settings/manage-settings.component.ts @@ -10,7 +10,7 @@ import {WikiLink} from "../../_models/wiki"; import {SettingItemComponent} from "../../settings/_components/setting-item/setting-item.component"; import {SettingSwitchComponent} from "../../settings/_components/setting-switch/setting-switch.component"; import {ConfirmService} from "../../shared/confirm.service"; -import {debounceTime, distinctUntilChanged, filter, switchMap, tap} from "rxjs"; +import {catchError, debounceTime, distinctUntilChanged, filter, of, switchMap, tap} from "rxjs"; import {takeUntilDestroyed} from "@angular/core/rxjs-interop"; import {DefaultValuePipe} from "../../_pipes/default-value.pipe"; import {EnterBlurDirective} from "../../_directives/enter-blur.directive"; @@ -40,6 +40,7 @@ export class ManageSettingsComponent implements OnInit { settingsForm: FormGroup = new FormGroup({}); taskFrequencies: Array = []; logLevels: Array = []; + isDocker: boolean = false; allowStatsTooltip = translate('manage-settings.allow-stats-tooltip-part-1') + ' - -
- @if (libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book) { - {{volume.name}} - } @else { - {{volume.chapters[0].titleName}} - } -
- - @if (actions && actions.length > 0) { - - - + @if ((libraryType === LibraryType.LightNovel || libraryType === LibraryType.Book)) { + @if (volume.name) { +
+
+ {{volume.name}} +
+
} - + } @else if (volume.chapters[0].titleName) { +
+
+ {{volume.chapters[0].titleName}} +
+
+ } }
diff --git a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts index 3a3087632..22405364f 100644 --- a/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts +++ b/UI/Web/src/app/reading-list/_components/draggable-ordered-list/draggable-ordered-list.component.ts @@ -82,7 +82,7 @@ export class DraggableOrderedListComponent { get BufferAmount() { - return Math.min(this.items.length / 20, 20); + return Math.floor(Math.min(this.items.length / 20, 20)); } constructor() { diff --git a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts index 890237be4..68d2ac4dd 100644 --- a/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts +++ b/UI/Web/src/app/series-detail/_components/series-detail/series-detail.component.ts @@ -115,6 +115,7 @@ import {CoverImageComponent} from "../../../_single-module/cover-image/cover-ima import {DefaultModalOptions} from "../../../_models/default-modal-options"; import {LicenseService} from "../../../_services/license.service"; import {PageBookmark} from "../../../_models/readers/page-bookmark"; +import {VolumeRemovedEvent} from "../../../_models/events/volume-removed-event"; enum TabID { @@ -353,11 +354,25 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { this.cdRef.markForCheck(); break; case Action.Delete: - await this.actionService.deleteMultipleChapters(seriesId, chapters, () => { - // No need to update the page as the backend will spam volume/chapter deletions - this.bulkSelectionService.deselectAll(); - this.cdRef.markForCheck(); - }); + if (chapters.length > 0) { + await this.actionService.deleteMultipleChapters(seriesId, chapters, () => { + // No need to update the page as the backend will spam volume/chapter deletions + this.bulkSelectionService.deselectAll(); + this.cdRef.markForCheck(); + }); + + // It's not possible to select both chapters and volumes + break; + } + + if (selectedVolumeIds.length > 0) { + await this.actionService.deleteMultipleVolumes(selectedVolumeIds, () => { + // No need to update the page as the backend will spam volume deletions + this.bulkSelectionService.deselectAll(); + this.cdRef.markForCheck(); + }); + } + break; } } @@ -486,6 +501,11 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { const removedEvent = event.payload as ChapterRemovedEvent; if (removedEvent.seriesId !== this.seriesId) return; this.loadPageSource.next(false); + } else if (event.event === EVENTS.VolumeRemoved) { + const volumeRemoveEvent = event.payload as VolumeRemovedEvent; + if (volumeRemoveEvent.seriesId === this.seriesId) { + this.loadPageSource.next(false); + } } }); @@ -660,7 +680,7 @@ export class SeriesDetailComponent implements OnInit, AfterContentChecked { case (Action.Delete): await this.actionService.deleteChapter(chapter.id, (success) => { if (!success) return; - + this.chapters = this.chapters.filter(c => c.id != chapter.id); this.cdRef.markForCheck(); }); diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json index 4d8a07210..dc409848f 100644 --- a/UI/Web/src/assets/langs/en.json +++ b/UI/Web/src/assets/langs/en.json @@ -2628,6 +2628,7 @@ "alert-long-running": "This is a long running process. Please give it the time to complete before invoking again.", "confirm-delete-multiple-series": "Are you sure you want to delete {{count}} series? It will not modify files on disk.", "confirm-delete-multiple-chapters": "Are you sure you want to delete {{count}} chapter/volumes? It will not modify files on disk.", + "confirm-delete-multiple-volumes": "Are you sure you want to delete {{count}} volumes? It will not modify files on disk.", "confirm-delete-series": "Are you sure you want to delete this series? It will not modify files on disk.", "confirm-delete-chapter": "Are you sure you want to delete this chapter? It will not modify files on disk.", "confirm-delete-volume": "Are you sure you want to delete this volume? It will not modify files on disk.",