diff --git a/API/Helpers/AutoMapperProfiles.cs b/API/Helpers/AutoMapperProfiles.cs
index 189ced83e..06a1a4b2e 100644
--- a/API/Helpers/AutoMapperProfiles.cs
+++ b/API/Helpers/AutoMapperProfiles.cs
@@ -117,6 +117,10 @@ public class AutoMapperProfiles : Profile
opt =>
opt.MapFrom(src =>
src.People.Where(p => p.Role == PersonRole.Inker).OrderBy(p => p.NormalizedName)))
+ .ForMember(dest => dest.Imprints,
+ opt =>
+ opt.MapFrom(src =>
+ src.People.Where(p => p.Role == PersonRole.Imprint).OrderBy(p => p.NormalizedName)))
.ForMember(dest => dest.Letterers,
opt =>
opt.MapFrom(src =>
diff --git a/API/Startup.cs b/API/Startup.cs
index 1acc5638a..6a5424303 100644
--- a/API/Startup.cs
+++ b/API/Startup.cs
@@ -137,14 +137,14 @@ public class Startup
{
c.SwaggerDoc("v1", new OpenApiInfo
{
- Version = BuildInfo.Version.ToString(),
+ Version = "2.0",
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.",
+ 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 v{BuildInfo.Version.ToString()}",
License = new OpenApiLicense
{
Name = "GPL-3.0",
Url = new Uri("https://github.com/Kareadita/Kavita/blob/develop/LICENSE")
- }
+ },
});
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html
index 4bcf433fa..c1eb262b6 100644
--- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html
+++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.html
@@ -1,21 +1,23 @@
-
0">
-
-
{{heading}}
-
-
-
= Breakpoint.Desktop ? 30 : 4">
-
-
-
+@if (tags && tags.length > 0) {
+
+
+
{{heading}}
+
+
+
= Breakpoint.Desktop ? 30 : 4">
+
+ @if(itemTemplate) {
+
-
-
-
-
-
+ } @else {
+
+
+
+ }
-
-
+
+
-
+}
+
diff --git a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts
index 2741a4a75..e4d25d177 100644
--- a/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts
+++ b/UI/Web/src/app/series-detail/_components/metadata-detail/metadata-detail.component.ts
@@ -1,5 +1,5 @@
import {ChangeDetectionStrategy, Component, ContentChild, inject, Input, TemplateRef} from '@angular/core';
-import {CommonModule} from '@angular/common';
+import {CommonModule, NgTemplateOutlet} from '@angular/common';
import {A11yClickDirective} from "../../../shared/a11y-click.directive";
import {BadgeExpanderComponent} from "../../../shared/badge-expander/badge-expander.component";
import {TagBadgeComponent, TagBadgeCursor} from "../../../shared/tag-badge/tag-badge.component";
@@ -11,7 +11,7 @@ import {Breakpoint, UtilityService} from "../../../shared/_services/utility.serv
@Component({
selector: 'app-metadata-detail',
standalone: true,
- imports: [CommonModule, A11yClickDirective, BadgeExpanderComponent, TagBadgeComponent],
+ imports: [A11yClickDirective, BadgeExpanderComponent, TagBadgeComponent, NgTemplateOutlet],
templateUrl: './metadata-detail.component.html',
styleUrls: ['./metadata-detail.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
diff --git a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
index 4ec35388a..0d0037479 100644
--- a/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
+++ b/UI/Web/src/app/series-detail/_components/series-metadata-detail/series-metadata-detail.component.html
@@ -123,14 +123,14 @@
-
-
+
+ {{item.name}}
-
-
+
+ {{item.name}}
@@ -141,8 +141,8 @@
-
-
+
+ {{item.name}}
diff --git a/openapi.json b/openapi.json
index e8b4eaf3f..d22973612 100644
--- a/openapi.json
+++ b/openapi.json
@@ -2,12 +2,12 @@
"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.",
+ "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.17",
"license": {
"name": "GPL-3.0",
"url": "https://github.com/Kareadita/Kavita/blob/develop/LICENSE"
},
- "version": "0.8.1.15"
+ "version": "2.0"
},
"servers": [
{
@@ -22251,6 +22251,729 @@
"description": "Responsible for all things Want To Read"
}
]
+} }
+ },
+ "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": "OPDS2",
+ "description": "All APIs for the OPDS 2.0 Implementation"
+ },
+ {
+ "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"
+ }
+ ]
+} },
+ "token": {
+ "type": "string",
+ "nullable": true
+ },
+ "refreshToken": {
+ "type": "string",
+ "nullable": true
+ },
+ "apiKey": {
+ "type": "string",
+ "nullable": true
+ },
+ "preferences": {
+ "$ref": "#/components/schemas/UserPreferencesDto"
+ },
+ "ageRestriction": {
+ "$ref": "#/components/schemas/AgeRestrictionDto"
+ },
+ "kavitaVersion": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "additionalProperties": false
+ },
+ "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"
+ }
+ ]
} "object",
"properties": {
"seriesId": {