diff --git a/API/Controllers/ReadingListController.cs b/API/Controllers/ReadingListController.cs
index 11a50d614..db0b134ef 100644
--- a/API/Controllers/ReadingListController.cs
+++ b/API/Controllers/ReadingListController.cs
@@ -491,4 +491,59 @@ public class ReadingListController : BaseApiController
if (string.IsNullOrEmpty(name)) return true;
return Ok(await _unitOfWork.ReadingListRepository.ReadingListExists(name));
}
+
+
+
+ ///
+ /// Promote/UnPromote multiple reading lists in one go. Will only update the authenticated user's reading lists and will only work if the user has promotion role
+ ///
+ ///
+ ///
+ [HttpPost("promote-multiple")]
+ public async Task PromoteMultipleReadingLists(PromoteReadingListsDto dto)
+ {
+ // This needs to take into account owner as I can select other users cards
+ var userId = User.GetUserId();
+ if (!User.IsInRole(PolicyConstants.PromoteRole) && !User.IsInRole(PolicyConstants.AdminRole))
+ {
+ return BadRequest(await _localizationService.Translate(userId, "permission-denied"));
+ }
+
+ var readingLists = await _unitOfWork.ReadingListRepository.GetReadingListsByIds(dto.ReadingListIds);
+
+ foreach (var readingList in readingLists)
+ {
+ if (readingList.AppUserId != userId) continue;
+ readingList.Promoted = dto.Promoted;
+ _unitOfWork.ReadingListRepository.Update(readingList);
+ }
+
+ if (!_unitOfWork.HasChanges()) return Ok();
+ await _unitOfWork.CommitAsync();
+
+ return Ok();
+ }
+
+
+ ///
+ /// Delete multiple reading lists in one go
+ ///
+ ///
+ ///
+ [HttpPost("delete-multiple")]
+ public async Task DeleteMultipleReadingLists(DeleteReadingListsDto dto)
+ {
+ // This needs to take into account owner as I can select other users cards
+ var user = await _unitOfWork.UserRepository.GetUserByIdAsync(User.GetUserId(), AppUserIncludes.ReadingLists);
+ if (user == null) return Unauthorized();
+
+ user.ReadingLists = user.ReadingLists.Where(uc => !dto.ReadingListIds.Contains(uc.Id)).ToList();
+ _unitOfWork.UserRepository.Update(user);
+
+
+ if (!_unitOfWork.HasChanges()) return Ok();
+ await _unitOfWork.CommitAsync();
+
+ return Ok();
+ }
}
diff --git a/API/DTOs/ReadingLists/DeleteReadingListsDto.cs b/API/DTOs/ReadingLists/DeleteReadingListsDto.cs
new file mode 100644
index 000000000..8417f8132
--- /dev/null
+++ b/API/DTOs/ReadingLists/DeleteReadingListsDto.cs
@@ -0,0 +1,10 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace API.DTOs.ReadingLists;
+
+public class DeleteReadingListsDto
+{
+ [Required]
+ public IList ReadingListIds { get; set; }
+}
diff --git a/API/DTOs/ReadingLists/PromoteReadingListsDto.cs b/API/DTOs/ReadingLists/PromoteReadingListsDto.cs
new file mode 100644
index 000000000..f64bbb5ca
--- /dev/null
+++ b/API/DTOs/ReadingLists/PromoteReadingListsDto.cs
@@ -0,0 +1,9 @@
+using System.Collections.Generic;
+
+namespace API.DTOs.ReadingLists;
+
+public class PromoteReadingListsDto
+{
+ public IList ReadingListIds { get; init; }
+ public bool Promoted { get; init; }
+}
diff --git a/API/Data/Repositories/ReadingListRepository.cs b/API/Data/Repositories/ReadingListRepository.cs
index c04328116..a1d2d754e 100644
--- a/API/Data/Repositories/ReadingListRepository.cs
+++ b/API/Data/Repositories/ReadingListRepository.cs
@@ -49,6 +49,7 @@ public interface IReadingListRepository
Task> GetAllWithCoversInDifferentEncoding(EncodeFormat encodeFormat);
Task RemoveReadingListsWithoutSeries();
Task GetReadingListByTitleAsync(string name, int userId, ReadingListIncludes includes = ReadingListIncludes.Items);
+ Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items);
}
public class ReadingListRepository : IReadingListRepository
@@ -156,6 +157,15 @@ public class ReadingListRepository : IReadingListRepository
.FirstOrDefaultAsync(x => x.NormalizedTitle != null && x.NormalizedTitle.Equals(normalized) && x.AppUserId == userId);
}
+ public async Task> GetReadingListsByIds(IList ids, ReadingListIncludes includes = ReadingListIncludes.Items)
+ {
+ return await _context.ReadingList
+ .Where(c => ids.Contains(c.Id))
+ .Includes(includes)
+ .AsSplitQuery()
+ .ToListAsync();
+ }
+
public void Remove(ReadingListItem item)
{
_context.ReadingListItem.Remove(item);
diff --git a/UI/Web/src/app/_services/action-factory.service.ts b/UI/Web/src/app/_services/action-factory.service.ts
index ecd2ee604..8ee9deef5 100644
--- a/UI/Web/src/app/_services/action-factory.service.ts
+++ b/UI/Web/src/app/_services/action-factory.service.ts
@@ -550,7 +550,7 @@ export class ActionFactoryService {
}
],
},
- // RBS will handle rendering this, so non-admins with download are appicable
+ // RBS will handle rendering this, so non-admins with download are applicable
{
action: Action.Download,
title: 'download',
@@ -583,6 +583,20 @@ export class ActionFactoryService {
class: 'danger',
children: [],
},
+ {
+ action: Action.Promote,
+ title: 'promote',
+ callback: this.dummyCallback,
+ requiresAdmin: false,
+ children: [],
+ },
+ {
+ action: Action.UnPromote,
+ title: 'unpromote',
+ callback: this.dummyCallback,
+ requiresAdmin: false,
+ children: [],
+ },
];
this.bookmarkActions = [
diff --git a/UI/Web/src/app/_services/action.service.ts b/UI/Web/src/app/_services/action.service.ts
index 22b8d862d..938b70315 100644
--- a/UI/Web/src/app/_services/action.service.ts
+++ b/UI/Web/src/app/_services/action.service.ts
@@ -24,6 +24,7 @@ import {UserCollection} from "../_models/collection-tag";
import {CollectionTagService} from "./collection-tag.service";
import {SmartFilter} from "../_models/metadata/v2/smart-filter";
import {FilterService} from "./filter.service";
+import {ReadingListService} from "./reading-list.service";
export type LibraryActionCallback = (library: Partial) => void;
export type SeriesActionCallback = (series: Series) => void;
@@ -48,7 +49,8 @@ export class ActionService implements OnDestroy {
constructor(private libraryService: LibraryService, private seriesService: SeriesService,
private readerService: ReaderService, private toastr: ToastrService, private modalService: NgbModal,
private confirmService: ConfirmService, private memberService: MemberService, private deviceService: DeviceService,
- private readonly collectionTagService: CollectionTagService, private filterService: FilterService) { }
+ private readonly collectionTagService: CollectionTagService, private filterService: FilterService,
+ private readonly readingListService: ReadingListService) { }
ngOnDestroy() {
this.onDestroy.next();
@@ -386,7 +388,7 @@ export class ActionService implements OnDestroy {
}
/**
- * Mark all series as Unread.
+ * Mark all collections as promoted/unpromoted.
* @param collections UserCollection, should have id, pagesRead populated
* @param promoted boolean, promoted state
* @param callback Optional callback to perform actions after API completes
@@ -422,6 +424,43 @@ export class ActionService implements OnDestroy {
});
}
+ /**
+ * Mark all reading lists as promoted/unpromoted.
+ * @param readingLists UserCollection, should have id, pagesRead populated
+ * @param promoted boolean, promoted state
+ * @param callback Optional callback to perform actions after API completes
+ */
+ promoteMultipleReadingLists(readingLists: Array, promoted: boolean, callback?: BooleanActionCallback) {
+ this.readingListService.promoteMultipleReadingLists(readingLists.map(v => v.id), promoted).pipe(take(1)).subscribe(() => {
+ if (promoted) {
+ this.toastr.success(translate('toasts.reading-list-promoted'));
+ } else {
+ this.toastr.success(translate('toasts.reading-list-unpromoted'));
+ }
+
+ if (callback) {
+ callback(true);
+ }
+ });
+ }
+
+ /**
+ * Deletes multiple collections
+ * @param readingLists ReadingList, should have id
+ * @param callback Optional callback to perform actions after API completes
+ */
+ async deleteMultipleReadingLists(readingLists: Array, callback?: BooleanActionCallback) {
+ if (!await this.confirmService.confirm(translate('toasts.confirm-delete-reading-list'))) return;
+
+ this.readingListService.deleteMultipleReadingLists(readingLists.map(v => v.id)).pipe(take(1)).subscribe(() => {
+ this.toastr.success(translate('toasts.reading-lists-deleted'));
+
+ if (callback) {
+ callback(true);
+ }
+ });
+ }
+
addMultipleToReadingList(seriesId: number, volumes: Array, chapters?: Array, callback?: BooleanActionCallback) {
if (this.readingListModalRef != null) { return; }
this.readingListModalRef = this.modalService.open(AddToListModalComponent, { scrollable: true, size: 'md', fullscreen: 'md' });
diff --git a/UI/Web/src/app/_services/reading-list.service.ts b/UI/Web/src/app/_services/reading-list.service.ts
index 4789fe67d..7afe5fa3c 100644
--- a/UI/Web/src/app/_services/reading-list.service.ts
+++ b/UI/Web/src/app/_services/reading-list.service.ts
@@ -8,7 +8,7 @@ import { PaginatedResult } from '../_models/pagination';
import { ReadingList, ReadingListItem } from '../_models/reading-list';
import { CblImportSummary } from '../_models/reading-list/cbl/cbl-import-summary';
import { TextResonse } from '../_types/text-response';
-import { ActionItem } from './action-factory.service';
+import {Action, ActionItem} from './action-factory.service';
@Injectable({
providedIn: 'root'
@@ -87,9 +87,15 @@ export class ReadingListService {
return this.httpClient.post(this.baseUrl + 'readinglist/remove-read?readingListId=' + readingListId, {}, TextResonse);
}
- actionListFilter(action: ActionItem, readingList: ReadingList, isAdmin: boolean) {
- if (readingList?.promoted && !isAdmin) return false;
+ actionListFilter(action: ActionItem, readingList: ReadingList, canPromote: boolean) {
+
+ const isPromotionAction = action.action == Action.Promote || action.action == Action.UnPromote;
+
+ if (isPromotionAction) return canPromote;
return true;
+
+ // if (readingList?.promoted && !isAdmin) return false;
+ // return true;
}
nameExists(name: string) {
@@ -107,4 +113,14 @@ export class ReadingListService {
getCharacters(readingListId: number) {
return this.httpClient.get>(this.baseUrl + 'readinglist/characters?readingListId=' + readingListId);
}
+
+ promoteMultipleReadingLists(listIds: Array, promoted: boolean) {
+ return this.httpClient.post(this.baseUrl + 'readinglist/promote-multiple', {readingListIds: listIds, promoted}, TextResonse);
+ }
+
+ deleteMultipleReadingLists(listIds: Array) {
+ return this.httpClient.post(this.baseUrl + 'readinglist/delete-multiple', {readingListIds: listIds}, TextResonse);
+ }
+
+
}
diff --git a/UI/Web/src/app/cards/bulk-selection.service.ts b/UI/Web/src/app/cards/bulk-selection.service.ts
index e1d625329..0572847ed 100644
--- a/UI/Web/src/app/cards/bulk-selection.service.ts
+++ b/UI/Web/src/app/cards/bulk-selection.service.ts
@@ -4,7 +4,7 @@ import {ReplaySubject} from 'rxjs';
import {filter} from 'rxjs/operators';
import {Action, ActionFactoryService, ActionItem} from '../_services/action-factory.service';
-type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark' | 'sideNavStream' | 'collection';
+type DataSource = 'volume' | 'chapter' | 'special' | 'series' | 'bookmark' | 'sideNavStream' | 'collection' | 'readingList';
/**
* Responsible for handling selections on cards. Can handle multiple card sources next to each other in different loops.
@@ -159,6 +159,10 @@ export class BulkSelectionService {
return this.applyFilterToList(this.actionFactory.getCollectionTagActions(callback), [Action.Promote, Action.UnPromote, Action.Delete]);
}
+ if (Object.keys(this.selectedCards).filter(item => item === 'readingList').length > 0) {
+ return this.applyFilterToList(this.actionFactory.getReadingListActions(callback), [Action.Promote, Action.UnPromote, Action.Delete]);
+ }
+
return this.applyFilterToList(this.actionFactory.getVolumeActions(callback), allowedActions);
}
diff --git a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html
index 4143e9c4a..7ec5a843b 100644
--- a/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html
+++ b/UI/Web/src/app/collections/_components/all-collections/all-collections.component.html
@@ -4,6 +4,7 @@
{{t('item-count', {num: collections.length | number})}}
+
{{t('title')}}
- {{t('item-count', {num: pagination.totalItems | number})}}
+ @if (pagination) {
+ {{t('item-count', {num: pagination.totalItems | number})}}
+ }
+
+
+ (clicked)="handleClick(item)"
+ (selection)="bulkSelectionService.handleCardSelection('readingList', position, lists.length, $event)"
+ [selected]="bulkSelectionService.isCardSelected('readingList', position)" [allowSelection]="true">
diff --git a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts
index 4fb54d7f5..8fb7d7410 100644
--- a/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts
+++ b/UI/Web/src/app/reading-list/_components/reading-lists/reading-lists.component.ts
@@ -1,4 +1,4 @@
-import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, OnInit} from '@angular/core';
+import {ChangeDetectionStrategy, ChangeDetectorRef, Component, HostListener, inject, OnInit} from '@angular/core';
import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
@@ -21,6 +21,12 @@ import {translate, TranslocoDirective, TranslocoService} from "@ngneat/transloco
import {CardActionablesComponent} from "../../../_single-module/card-actionables/card-actionables.component";
import {Title} from "@angular/platform-browser";
import {WikiLink} from "../../../_models/wiki";
+import {BulkSelectionService} from "../../../cards/bulk-selection.service";
+import {BulkOperationsComponent} from "../../../cards/bulk-operations/bulk-operations.component";
+import {KEY_CODES} from "../../../shared/_services/utility.service";
+import {UserCollection} from "../../../_models/collection-tag";
+import {User} from "../../../_models/user";
+import {takeUntilDestroyed} from "@angular/core/rxjs-interop";
@Component({
selector: 'app-reading-lists',
@@ -28,30 +34,51 @@ import {WikiLink} from "../../../_models/wiki";
styleUrls: ['./reading-lists.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
- imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgIf, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoDirective]
+ imports: [SideNavCompanionBarComponent, CardActionablesComponent, NgIf, CardDetailLayoutComponent, CardItemComponent, DecimalPipe, TranslocoDirective, BulkOperationsComponent]
})
export class ReadingListsComponent implements OnInit {
+ public readonly bulkSelectionService = inject(BulkSelectionService);
+ public readonly actionService = inject(ActionService);
+
protected readonly WikiLink = WikiLink;
lists: ReadingList[] = [];
loadingLists = false;
pagination!: Pagination;
isAdmin: boolean = false;
+ hasPromote: boolean = false;
jumpbarKeys: Array = [];
actions: {[key: number]: Array>} = {};
globalActions: Array> = [{action: Action.Import, title: 'import-cbl', children: [], requiresAdmin: true, callback: this.importCbl.bind(this)}];
trackByIdentity = (index: number, item: ReadingList) => `${item.id}_${item.title}`;
- translocoService = inject(TranslocoService);
+ @HostListener('document:keydown.shift', ['$event'])
+ handleKeypress(event: KeyboardEvent) {
+ if (event.key === KEY_CODES.SHIFT) {
+ this.bulkSelectionService.isShiftDown = true;
+ }
+ }
+
+ @HostListener('document:keyup.shift', ['$event'])
+ handleKeyUp(event: KeyboardEvent) {
+ if (event.key === KEY_CODES.SHIFT) {
+ this.bulkSelectionService.isShiftDown = false;
+ }
+ }
+
constructor(private readingListService: ReadingListService, public imageService: ImageService, private actionFactoryService: ActionFactoryService,
- private accountService: AccountService, private toastr: ToastrService, private router: Router, private actionService: ActionService,
+ private accountService: AccountService, private toastr: ToastrService, private router: Router,
private jumpbarService: JumpbarService, private readonly cdRef: ChangeDetectorRef, private ngbModal: NgbModal, private titleService: Title) { }
ngOnInit(): void {
this.accountService.currentUser$.pipe(take(1)).subscribe(user => {
if (user) {
this.isAdmin = this.accountService.hasAdminRole(user);
+ this.hasPromote = this.accountService.hasPromoteRole(user);
+
+ this.cdRef.markForCheck();
+
this.loadPage();
this.titleService.setTitle('Kavita - ' + translate('side-nav.reading-lists'));
}
@@ -59,8 +86,10 @@ export class ReadingListsComponent implements OnInit {
}
getActions(readingList: ReadingList) {
+ const d = this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
+ .filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
return this.actionFactoryService.getReadingListActions(this.handleReadingListActionCallback.bind(this))
- .filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin));
+ .filter(action => this.readingListService.actionListFilter(action, readingList, this.isAdmin || this.hasPromote));
}
performAction(action: ActionItem, readingList: ReadingList) {
@@ -85,7 +114,7 @@ export class ReadingListsComponent implements OnInit {
switch(action.action) {
case Action.Delete:
this.readingListService.delete(readingList.id).subscribe(() => {
- this.toastr.success(this.translocoService.translate('toasts.reading-list-deleted'));
+ this.toastr.success(translate('toasts.reading-list-deleted'));
this.loadPage();
});
break;
@@ -126,4 +155,33 @@ export class ReadingListsComponent implements OnInit {
handleClick(list: ReadingList) {
this.router.navigateByUrl('lists/' + list.id);
}
+
+ bulkActionCallback = (action: ActionItem, data: any) => {
+ const selectedReadingListIndexies = this.bulkSelectionService.getSelectedCardsForSource('readingList');
+ const selectedReadingLists = this.lists.filter((col, index: number) => selectedReadingListIndexies.includes(index + ''));
+
+ switch (action.action) {
+ case Action.Promote:
+ this.actionService.promoteMultipleReadingLists(selectedReadingLists, true, (success) => {
+ if (!success) return;
+ this.bulkSelectionService.deselectAll();
+ this.loadPage();
+ });
+ break;
+ case Action.UnPromote:
+ this.actionService.promoteMultipleReadingLists(selectedReadingLists, false, (success) => {
+ if (!success) return;
+ this.bulkSelectionService.deselectAll();
+ this.loadPage();
+ });
+ break;
+ case Action.Delete:
+ this.actionService.deleteMultipleReadingLists(selectedReadingLists, (successful) => {
+ if (!successful) return;
+ this.loadPage();
+ this.bulkSelectionService.deselectAll();
+ });
+ break;
+ }
+ }
}
diff --git a/UI/Web/src/assets/langs/en.json b/UI/Web/src/assets/langs/en.json
index 30f860801..da092a9b0 100644
--- a/UI/Web/src/assets/langs/en.json
+++ b/UI/Web/src/assets/langs/en.json
@@ -2134,8 +2134,10 @@
"no-series-collection-warning": "Warning! No series are selected, saving will delete the Collection. Are you sure you want to continue?",
"collection-updated": "Collection updated",
"reading-list-deleted": "Reading list deleted",
+ "reading-lists-deleted": "Reading lists deleted",
"reading-list-updated": "Reading list updated",
"confirm-delete-reading-list": "Are you sure you want to delete the reading list? This cannot be undone.",
+ "confirm-delete-reading-lists": "Are you sure you want to delete the reading lists? This cannot be undone.",
"item-removed": "Item removed",
"nothing-to-remove": "Nothing to remove",
"series-added-to-reading-list": "Series added to reading list",
@@ -2209,6 +2211,8 @@
"collection-not-owned": "You do not own this collection",
"collections-promoted": "Collections promoted",
"collections-unpromoted": "Collections un-promoted",
+ "reading-lists-promoted": "Reading Lists promoted",
+ "reading-lists-unpromoted": "Reading Lists un-promoted",
"confirm-delete-collections": "Are you sure you want to delete multiple collections?",
"collections-deleted": "Collections deleted",
"pdf-book-mode-screen-size": "Screen too small for Book mode",
diff --git a/openapi.json b/openapi.json
index 11956030d..891190de4 100644
--- a/openapi.json
+++ b/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.0.1",
"info": {
"title": "Kavita",
- "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.1.18",
+ "description": "Kavita provides a set of APIs that are authenticated by JWT. JWT token can be copied from local storage. Assume all fields of a payload are required. Built against v0.8.1.19",
"license": {
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
@@ -7150,6 +7150,72 @@
}
}
},
+ "/api/ReadingList/promote-multiple": {
+ "post": {
+ "tags": [
+ "ReadingList"
+ ],
+ "summary": "Promote/UnPromote multiple reading lists in one go. Will only update the authenticated user's reading lists and will only work if the user has promotion role",
+ "requestBody": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PromoteReadingListsDto"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PromoteReadingListsDto"
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "$ref": "#/components/schemas/PromoteReadingListsDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
+ "/api/ReadingList/delete-multiple": {
+ "post": {
+ "tags": [
+ "ReadingList"
+ ],
+ "summary": "Delete multiple reading lists in one go",
+ "requestBody": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeleteReadingListsDto"
+ }
+ },
+ "text/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeleteReadingListsDto"
+ }
+ },
+ "application/*+json": {
+ "schema": {
+ "$ref": "#/components/schemas/DeleteReadingListsDto"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "OK"
+ }
+ }
+ }
+ },
"/api/Recommended/quick-reads": {
"get": {
"tags": [
@@ -15836,6 +15902,22 @@
},
"additionalProperties": false
},
+ "DeleteReadingListsDto": {
+ "required": [
+ "readingListIds"
+ ],
+ "type": "object",
+ "properties": {
+ "readingListIds": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
"DeleteSeriesDto": {
"type": "object",
"properties": {
@@ -17991,6 +18073,23 @@
},
"additionalProperties": false
},
+ "PromoteReadingListsDto": {
+ "type": "object",
+ "properties": {
+ "readingListIds": {
+ "type": "array",
+ "items": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "nullable": true
+ },
+ "promoted": {
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ },
"PublicationStatusStatCount": {
"type": "object",
"properties": {
@@ -22251,4 +22350,628 @@
"description": "Responsible for all things Want To Read"
}
]
+}"UserDtoICount": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "$ref": "#/components/schemas/UserDto"
+ },
+ "count": {
+ "type": "integer",
+ "format": "int64"
+ }
+ },
+ "additionalProperties": false
+ },
+ "UserPreferencesDto": {
+ "required": [
+ "autoCloseMenu",
+ "backgroundColor",
+ "blurUnreadSummaries",
+ "bookReaderFontFamily",
+ "bookReaderFontSize",
+ "bookReaderImmersiveMode",
+ "bookReaderLayoutMode",
+ "bookReaderLineSpacing",
+ "bookReaderMargin",
+ "bookReaderReadingDirection",
+ "bookReaderTapToPaginate",
+ "bookReaderThemeName",
+ "bookReaderWritingStyle",
+ "collapseSeriesRelationships",
+ "emulateBook",
+ "globalPageLayoutMode",
+ "layoutMode",
+ "locale",
+ "noTransitions",
+ "pageSplitOption",
+ "pdfLayoutMode",
+ "pdfScrollMode",
+ "pdfSpreadMode",
+ "pdfTheme",
+ "promptForDownloadSize",
+ "readerMode",
+ "readingDirection",
+ "scalingOption",
+ "shareReviews",
+ "showScreenHints",
+ "swipeToPaginate",
+ "theme"
+ ],
+ "type": "object",
+ "properties": {
+ "readingDirection": {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "description": "Manga Reader Option: What direction should the next/prev page buttons go",
+ "format": "int32"
+ },
+ "scalingOption": {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "type": "integer",
+ "description": "Manga Reader Option: How should the image be scaled to screen",
+ "format": "int32"
+ },
+ "pageSplitOption": {
+ "enum": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "type": "integer",
+ "description": "Manga Reader Option: Which side of a split image should we show first",
+ "format": "int32"
+ },
+ "readerMode": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "integer",
+ "description": "Manga Reader Option: How the manga reader should perform paging or reading of the file\r\n\r\nWebtoon uses scrolling to page, LeftRight uses paging by clicking left/right side of reader, UpDown uses paging\r\nby clicking top/bottom sides of reader.\r\n",
+ "format": "int32"
+ },
+ "layoutMode": {
+ "enum": [
+ 1,
+ 2,
+ 3
+ ],
+ "type": "integer",
+ "description": "Manga Reader Option: How many pages to display in the reader at once",
+ "format": "int32"
+ },
+ "emulateBook": {
+ "type": "boolean",
+ "description": "Manga Reader Option: Emulate a book by applying a shadow effect on the pages"
+ },
+ "backgroundColor": {
+ "minLength": 1,
+ "type": "string",
+ "description": "Manga Reader Option: Background color of the reader"
+ },
+ "swipeToPaginate": {
+ "type": "boolean",
+ "description": "Manga Reader Option: Should swiping trigger pagination"
+ },
+ "autoCloseMenu": {
+ "type": "boolean",
+ "description": "Manga Reader Option: Allow the menu to close after 6 seconds without interaction"
+ },
+ "showScreenHints": {
+ "type": "boolean",
+ "description": "Manga Reader Option: Show screen hints to the user on some actions, ie) pagination direction change"
+ },
+ "bookReaderMargin": {
+ "type": "integer",
+ "description": "Book Reader Option: Override extra Margin",
+ "format": "int32"
+ },
+ "bookReaderLineSpacing": {
+ "type": "integer",
+ "description": "Book Reader Option: Override line-height",
+ "format": "int32"
+ },
+ "bookReaderFontSize": {
+ "type": "integer",
+ "description": "Book Reader Option: Override font size",
+ "format": "int32"
+ },
+ "bookReaderFontFamily": {
+ "minLength": 1,
+ "type": "string",
+ "description": "Book Reader Option: Maps to the default Kavita font-family (inherit) or an override"
+ },
+ "bookReaderTapToPaginate": {
+ "type": "boolean",
+ "description": "Book Reader Option: Allows tapping on side of screens to paginate"
+ },
+ "bookReaderReadingDirection": {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "description": "Book Reader Option: What direction should the next/prev page buttons go",
+ "format": "int32"
+ },
+ "bookReaderWritingStyle": {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "description": "Book Reader Option: What writing style should be used, horizontal or vertical.",
+ "format": "int32"
+ },
+ "theme": {
+ "$ref": "#/components/schemas/SiteThemeDto"
+ },
+ "bookReaderThemeName": {
+ "minLength": 1,
+ "type": "string"
+ },
+ "bookReaderLayoutMode": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "integer",
+ "format": "int32"
+ },
+ "bookReaderImmersiveMode": {
+ "type": "boolean",
+ "description": "Book Reader Option: A flag that hides the menu-ing system behind a click on the screen. This should be used with tap to paginate, but the app doesn't enforce this."
+ },
+ "globalPageLayoutMode": {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "description": "Global Site Option: If the UI should layout items as Cards or List items",
+ "format": "int32"
+ },
+ "blurUnreadSummaries": {
+ "type": "boolean",
+ "description": "UI Site Global Setting: If unread summaries should be blurred until expanded or unless user has read it already"
+ },
+ "promptForDownloadSize": {
+ "type": "boolean",
+ "description": "UI Site Global Setting: Should Kavita prompt user to confirm downloads that are greater than 100 MB."
+ },
+ "noTransitions": {
+ "type": "boolean",
+ "description": "UI Site Global Setting: Should Kavita disable CSS transitions"
+ },
+ "collapseSeriesRelationships": {
+ "type": "boolean",
+ "description": "When showing series, only parent series or series with no relationships will be returned"
+ },
+ "shareReviews": {
+ "type": "boolean",
+ "description": "UI Site Global Setting: Should series reviews be shared with all users in the server"
+ },
+ "locale": {
+ "minLength": 1,
+ "type": "string",
+ "description": "UI Site Global Setting: The language locale that should be used for the user"
+ },
+ "pdfTheme": {
+ "enum": [
+ 0,
+ 1
+ ],
+ "type": "integer",
+ "description": "PDF Reader: Theme of the Reader",
+ "format": "int32"
+ },
+ "pdfScrollMode": {
+ "enum": [
+ 0,
+ 1,
+ 3
+ ],
+ "type": "integer",
+ "description": "PDF Reader: Scroll mode of the reader",
+ "format": "int32"
+ },
+ "pdfLayoutMode": {
+ "enum": [
+ 0,
+ 2
+ ],
+ "type": "integer",
+ "description": "PDF Reader: Layout Mode of the reader",
+ "format": "int32"
+ },
+ "pdfSpreadMode": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "integer",
+ "description": "PDF Reader: Spread Mode of the reader",
+ "format": "int32"
+ }
+ },
+ "additionalProperties": false
+ },
+ "UserReadStatistics": {
+ "type": "object",
+ "properties": {
+ "totalPagesRead": {
+ "type": "integer",
+ "description": "Total number of pages read",
+ "format": "int64"
+ },
+ "totalWordsRead": {
+ "type": "integer",
+ "description": "Total number of words read",
+ "format": "int64"
+ },
+ "timeSpentReading": {
+ "type": "integer",
+ "description": "Total time spent reading based on estimates",
+ "format": "int64"
+ },
+ "chaptersRead": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "lastActive": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "avgHoursPerWeekSpentReading": {
+ "type": "number",
+ "format": "double"
+ },
+ "percentReadPerLibrary": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SingleStatCount"
+ },
+ "nullable": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "UserReviewDto": {
+ "type": "object",
+ "properties": {
+ "tagline": {
+ "type": "string",
+ "description": "A tagline for the review",
+ "nullable": true
+ },
+ "body": {
+ "type": "string",
+ "description": "The main review",
+ "nullable": true
+ },
+ "bodyJustText": {
+ "type": "string",
+ "description": "The main body with just text, for review preview",
+ "nullable": true
+ },
+ "seriesId": {
+ "type": "integer",
+ "description": "The series this is for",
+ "format": "int32"
+ },
+ "libraryId": {
+ "type": "integer",
+ "description": "The library this series belongs in",
+ "format": "int32"
+ },
+ "username": {
+ "type": "string",
+ "description": "The user who wrote this",
+ "nullable": true
+ },
+ "totalVotes": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "rating": {
+ "type": "number",
+ "format": "float"
+ },
+ "rawBody": {
+ "type": "string",
+ "nullable": true
+ },
+ "score": {
+ "type": "integer",
+ "description": "How many upvotes this review has gotten",
+ "format": "int32"
+ },
+ "siteUrl": {
+ "type": "string",
+ "description": "If External, the url of the review",
+ "nullable": true
+ },
+ "isExternal": {
+ "type": "boolean",
+ "description": "Does this review come from an external Source"
+ },
+ "provider": {
+ "enum": [
+ 0,
+ 1,
+ 2
+ ],
+ "type": "integer",
+ "description": "If this review is External, which Provider did it come from",
+ "format": "int32"
+ }
+ },
+ "additionalProperties": false,
+ "description": "Represents a User Review for a given Series"
+ },
+ "Volume": {
+ "required": [
+ "maxNumber",
+ "minNumber",
+ "name"
+ ],
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "name": {
+ "type": "string",
+ "description": "A String representation of the volume number. Allows for floats. Can also include a range (1-2).",
+ "nullable": true
+ },
+ "lookupName": {
+ "type": "string",
+ "description": "This is just the original Parsed volume number for lookups",
+ "nullable": true
+ },
+ "number": {
+ "type": "integer",
+ "description": "The minimum number in the Name field in Int form",
+ "format": "int32",
+ "deprecated": true
+ },
+ "minNumber": {
+ "type": "number",
+ "description": "The minimum number in the Name field",
+ "format": "float"
+ },
+ "maxNumber": {
+ "type": "number",
+ "description": "The maximum number in the Name field (same as Minimum if Name isn't a range)",
+ "format": "float"
+ },
+ "chapters": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Chapter"
+ },
+ "nullable": true
+ },
+ "created": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "lastModified": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "createdUtc": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "lastModifiedUtc": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "coverImage": {
+ "type": "string",
+ "description": "Absolute path to the (managed) image file",
+ "nullable": true
+ },
+ "pages": {
+ "type": "integer",
+ "description": "Total pages of all chapters in this volume",
+ "format": "int32"
+ },
+ "wordCount": {
+ "type": "integer",
+ "description": "Total Word count of all chapters in this volume.",
+ "format": "int64"
+ },
+ "minHoursToRead": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "maxHoursToRead": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "avgHoursToRead": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "series": {
+ "$ref": "#/components/schemas/Series"
+ },
+ "seriesId": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ "additionalProperties": false
+ },
+ "VolumeDto": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "minNumber": {
+ "type": "number",
+ "format": "float"
+ },
+ "maxNumber": {
+ "type": "number",
+ "format": "float"
+ },
+ "name": {
+ "type": "string",
+ "nullable": true
+ },
+ "number": {
+ "type": "integer",
+ "description": "This will map to MinNumber. Number was removed in v0.7.13.8/v0.7.14",
+ "format": "int32",
+ "deprecated": true
+ },
+ "pages": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "pagesRead": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "lastModifiedUtc": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "createdUtc": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "created": {
+ "type": "string",
+ "description": "When chapter was created in local server time",
+ "format": "date-time"
+ },
+ "lastModified": {
+ "type": "string",
+ "description": "When chapter was last modified in local server time",
+ "format": "date-time"
+ },
+ "seriesId": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "chapters": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ChapterDto"
+ },
+ "nullable": true
+ },
+ "minHoursToRead": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "maxHoursToRead": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "avgHoursToRead": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "securitySchemes": {
+ "Bearer": {
+ "type": "apiKey",
+ "description": "Please insert JWT with Bearer into field",
+ "name": "Authorization",
+ "in": "header"
+ }
+ }
+ },
+ "security": [
+ {
+ "Bearer": [ ]
+ }
+ ],
+ "tags": [
+ {
+ "name": "Account",
+ "description": "All Account matters"
+ },
+ {
+ "name": "Cbl",
+ "description": "Responsible for the CBL import flow"
+ },
+ {
+ "name": "Collection",
+ "description": "APIs for Collections"
+ },
+ {
+ "name": "Device",
+ "description": "Responsible interacting and creating Devices"
+ },
+ {
+ "name": "Download",
+ "description": "All APIs related to downloading entities from the system. Requires Download Role or Admin Role."
+ },
+ {
+ "name": "Filter",
+ "description": "This is responsible for Filter caching"
+ },
+ {
+ "name": "Image",
+ "description": "Responsible for servicing up images stored in Kavita for entities"
+ },
+ {
+ "name": "Panels",
+ "description": "For the Panels app explicitly"
+ },
+ {
+ "name": "Rating",
+ "description": "Responsible for providing external ratings for Series"
+ },
+ {
+ "name": "Reader",
+ "description": "For all things regarding reading, mainly focusing on non-Book related entities"
+ },
+ {
+ "name": "Search",
+ "description": "Responsible for the Search interface from the UI"
+ },
+ {
+ "name": "Stream",
+ "description": "Responsible for anything that deals with Streams (SmartFilters, ExternalSource, DashboardStream, SideNavStream)"
+ },
+ {
+ "name": "Tachiyomi",
+ "description": "All APIs are for Tachiyomi extension and app. They have hacks for our implementation and should not be used for any\r\nother purposes."
+ },
+ {
+ "name": "Upload",
+ "description": ""
+ },
+ {
+ "name": "WantToRead",
+ "description": "Responsible for all things Want To Read"
+ }
+ ]
}
\ No newline at end of file