Merged develop in

This commit is contained in:
Joseph Milazzo 2025-04-26 16:17:05 -05:00
commit d12a79892f
1443 changed files with 215765 additions and 44113 deletions

View file

@ -8,6 +8,12 @@ indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[*.json]
indent_size = 2
[en.json]
indent_size = 4
[*.html]
indent_size = 2

1
UI/Web/.gitignore vendored
View file

@ -2,3 +2,4 @@ node_modules/
test-results/
playwright-report/
i18n-cache-busting.json
e2e-tests/environments/environment.local.ts

View file

@ -4,7 +4,7 @@ This project was generated with [Angular CLI](https://github.com/angular/angular
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
Run `npm run start` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
Your backend must be served on port 5000.
## Code scaffolding
@ -25,11 +25,15 @@ Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.
Run `npx playwright test --reporter=line` or `npx playwright test` to run e2e tests.
## Connecting to your dev server via your phone
## Connecting to your dev server via your phone or any other compatible client on local network
ng serve --host 0.0.0.0
and update environment.ts to your local ip.
Update `IP` constant in `src/environments/environment.ts` to your dev machine's ip instead of `localhost`.
Run `npm run start`
## Notes:
- injected services should be at the top of the file
- all components must be standalone
# Update latest angular
`ng update @angular/core @angular/cli @typescript-eslint/parser @angular/localize @angular/compiler-cli @angular-devkit/build-angular @angular/cdk`

View file

@ -24,13 +24,14 @@
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"builder": "@angular/build:application",
"options": {
"outputPath": "dist",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
"@angular/localize/init",
"zone.js"
],
"inlineStyleLanguage": "scss",
"tsConfig": "tsconfig.app.json",
@ -55,7 +56,12 @@
},
"extractLicenses": false,
"optimization": false,
"namedChunks": true
"namedChunks": true,
"stylePreprocessorOptions": {
"sass": {
"silenceDeprecations": ["mixed-decls", "color-functions", "global-builtin", "import"]
}
}
},
"configurations": {
"production": {
@ -87,7 +93,7 @@
"defaultConfiguration": ""
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"builder": "@angular/build:dev-server",
"options": {
"sslKey": "./ssl/server.key",
"sslCert": "./ssl/server.crt",
@ -101,7 +107,7 @@
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"builder": "@angular/build:extract-i18n",
"options": {
"buildTarget": "kavita-webui:build"
}

View file

@ -14,6 +14,15 @@ function generateChecksum(str, algorithm, encoding) {
const result = {};
// Generate directory if it doesn't exist
const distFolderPath = './dist/';
const browserFolderPath = './dist/browser/';
if (!fs.existsSync(browserFolderPath)) {
console.log('Creating ./dist/browser folder');
fs.mkdirSync(distFolderPath, 0o744);
fs.mkdirSync(browserFolderPath, 0o744);
}
// Remove file if it exists
const cacheBustingFilePath = './i18n-cache-busting.json';
if (fs.existsSync(cacheBustingFilePath)) {

13043
UI/Web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,82 +3,83 @@
"version": "0.7.12.1",
"scripts": {
"ng": "ng",
"start": "npm run cache-locale && ng serve",
"start": "npm run cache-locale && ng serve --host 0.0.0.0",
"build": "npm run cache-locale && ng build",
"minify-langs": "node minify-json.js",
"cache-locale": "node hash-localization.js",
"cache-locale-prime": "node hash-localization-prime.js",
"prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale",
"sync-locale": "node sync-locales.js",
"prod": "npm run cache-locale-prime && ng build --configuration production && npm run minify-langs && npm run cache-locale",
"explore": "ng build --stats-json && webpack-bundle-analyzer dist/stats.json",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^17.1.0",
"@angular/cdk": "^17.1.0",
"@angular/common": "^17.1.0",
"@angular/compiler": "^17.1.0",
"@angular/core": "^17.1.0",
"@angular/forms": "^17.1.0",
"@angular/localize": "^17.1.0",
"@angular/platform-browser": "^17.1.0",
"@angular/platform-browser-dynamic": "^17.1.0",
"@angular/router": "^17.1.0",
"@fortawesome/fontawesome-free": "^6.5.1",
"@iharbeck/ngx-virtual-scroller": "^17.0.0",
"@iplab/ngx-file-upload": "^17.0.0",
"@microsoft/signalr": "^7.0.12",
"@ng-bootstrap/ng-bootstrap": "^16.0.0",
"@ngneat/transloco": "^6.0.4",
"@ngneat/transloco-locale": "^5.1.1",
"@ngneat/transloco-persist-lang": "^5.0.0",
"@ngneat/transloco-persist-translations": "^5.0.0",
"@ngneat/transloco-preload-langs": "^5.0.1",
"@angular-slider/ngx-slider": "^19.0.0",
"@angular/animations": "^19.2.5",
"@angular/cdk": "^19.2.8",
"@angular/common": "^19.2.5",
"@angular/compiler": "^19.2.5",
"@angular/core": "^19.2.5",
"@angular/forms": "^19.2.5",
"@angular/localize": "^19.2.5",
"@angular/platform-browser": "^19.2.5",
"@angular/platform-browser-dynamic": "^19.2.5",
"@angular/router": "^19.2.5",
"@fortawesome/fontawesome-free": "^6.7.2",
"@iharbeck/ngx-virtual-scroller": "^19.0.1",
"@iplab/ngx-file-upload": "^19.0.3",
"@jsverse/transloco": "^7.6.1",
"@jsverse/transloco-locale": "^7.0.1",
"@jsverse/transloco-persist-lang": "^7.0.2",
"@jsverse/transloco-persist-translations": "^7.0.1",
"@jsverse/transloco-preload-langs": "^7.0.1",
"@microsoft/signalr": "^8.0.7",
"@ng-bootstrap/ng-bootstrap": "^18.0.0",
"@popperjs/core": "^2.11.7",
"@swimlane/ngx-charts": "^20.5.0",
"@tweenjs/tween.js": "^21.0.0",
"@siemens/ngx-datatable": "^22.4.1",
"@swimlane/ngx-charts": "^22.0.0-alpha.0",
"@tweenjs/tween.js": "^25.0.0",
"bootstrap": "^5.3.2",
"charts.css": "^1.1.0",
"file-saver": "^2.0.5",
"luxon": "^3.4.4",
"luxon": "^3.6.1",
"ng-circle-progress": "^1.7.1",
"ng-lazyload-image": "^9.1.3",
"ng-select2-component": "^14.0.0",
"ngx-color-picker": "^16.0.0",
"ngx-extended-pdf-viewer": "^18.1.9",
"ng-select2-component": "^17.2.4",
"ngx-color-picker": "^19.0.0",
"ngx-extended-pdf-viewer": "^23.0.0-alpha.7",
"ngx-file-drop": "^16.0.0",
"ngx-slider-v2": "^17.0.0",
"ngx-stars": "^1.6.5",
"ngx-toaster": "^1.0.1",
"ngx-toastr": "^18.0.0",
"ngx-toastr": "^19.0.0",
"nosleep.js": "^0.12.0",
"rxjs": "^7.8.0",
"rxjs": "^7.8.2",
"screenfull": "^6.0.2",
"swiper": "^8.4.6",
"tslib": "^2.6.2",
"zone.js": "^0.14.2"
"tslib": "^2.8.1",
"zone.js": "^0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^17.1.0",
"@angular-eslint/builder": "^17.2.1",
"@angular-eslint/eslint-plugin": "^17.2.1",
"@angular-eslint/eslint-plugin-template": "^17.2.1",
"@angular-eslint/schematics": "^17.2.1",
"@angular-eslint/template-parser": "^17.2.1",
"@angular/cli": "^17.1.0",
"@angular/compiler-cli": "^17.1.0",
"@angular-eslint/builder": "^19.3.0",
"@angular-eslint/eslint-plugin": "^19.3.0",
"@angular-eslint/eslint-plugin-template": "^19.3.0",
"@angular-eslint/schematics": "^19.3.0",
"@angular-eslint/template-parser": "^19.3.0",
"@angular/build": "^19.2.6",
"@angular/cli": "^19.2.6",
"@angular/compiler-cli": "^19.2.5",
"@types/d3": "^7.4.3",
"@types/file-saver": "^2.0.7",
"@types/luxon": "^3.4.0",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.13.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint": "^8.54.0",
"@types/luxon": "^3.6.2",
"@types/node": "^22.13.13",
"@typescript-eslint/eslint-plugin": "^8.28.0",
"@typescript-eslint/parser": "^8.28.0",
"eslint": "^9.23.0",
"jsonminify": "^0.4.2",
"karma-coverage": "~2.2.0",
"ts-node": "~10.9.1",
"typescript": "^5.2.2",
"webpack-bundle-analyzer": "^4.10.1"
"typescript": "^5.5.4",
"webpack-bundle-analyzer": "^4.10.2"
}
}

View file

@ -0,0 +1,246 @@
$image-height: 232.91px;
$image-width: 160px;
.error-banner {
width: $image-width;
height: 18px;
background-color: var(--toast-error-bg-color);
font-size: 12px;
color: white;
text-transform: uppercase;
text-align: center;
position: absolute;
top: 0px;
right: 0px;
}
.selected-highlight {
outline: 2px solid var(--primary-color);
}
.progress-banner {
width: $image-width;
height: 5px;
.progress {
color: var(--card-progress-bar-color);
background-color: transparent;
}
}
.download {
width: 80px;
height: 80px;
position: absolute;
top: 25%;
right: 30%;
}
.badge-container {
border-radius: 4px;
display: block;
height: $image-height;
left: 0;
overflow: hidden;
pointer-events: none;
position: absolute;
top: 0;
width: 160px;
}
.not-read-badge {
position: absolute;
top: calc(-1 * (var(--card-progress-triangle-size) / 2));
right: -14px;
z-index: 1000;
height: var(--card-progress-triangle-size);
width: var(--card-progress-triangle-size);
background-color: var(--primary-color);
transform: rotate(45deg);
}
.bulk-mode {
position: absolute;
top: 5px;
left: 5px;
visibility: hidden;
&.always-show {
visibility: visible !important;
width: $image-width;
height: $image-height;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
color: var(--checkbox-bg-color);
}
}
.meta-title {
display: none;
visibility: hidden;
pointer-events: none;
border-width: 0;
}
.overlay {
&:hover {
.bulk-mode {
visibility: visible;
z-index: 110;
}
&:hover {
visibility: visible;
.overlay-information {
visibility: visible;
display: block;
}
& + .meta-title {
display: -webkit-box;
visibility: visible;
pointer-events: none;
}
}
.overlay-information {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 232.91px;
transition: all 0.2s;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
&:hover {
background-color: var(--card-overlay-hover-bg-color);
cursor: pointer;
}
.overlay-information--centered {
position: absolute;
background-color: rgba(0, 0, 0, 0.7);
border-radius: 50px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 115;
&:hover {
background-color: var(--primary-color) !important;
cursor: pointer;
}
}
}
}
.count {
top: 5px;
right: 10px;
position: absolute;
}
}
.card-actions {
z-index: 115;
}
.library {
font-size: 13px;
text-decoration: none;
margin-top: 0px;
}
.card-title-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 5px;
:first-child {
min-width: 22px;
}
.card-title {
font-size: 0.8rem;
margin: 0;
text-align: center;
max-width: 90px;
a {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.card-actions {
min-width: 15.82px;
}
.card-format {
min-width: 22px;
}
::ng-deep app-card-actionables .dropdown .dropdown-toggle {
padding: 0 5px;
}
.meta-title {
.card-title {
max-width: unset;
}
}
.card-title {
font-size: 0.8rem;
margin: 0;
padding: 10px 0;
text-align: center;
max-width: 120px;
a {
overflow: hidden;
text-overflow: ellipsis;
}
}
.card-body > div:nth-child(2) {
height: 40px;
overflow: hidden;
-webkit-line-clamp: 2;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
font-size: 0.8rem;
}
.overlay-information {
visibility: hidden;
display: none;
.card-title {
padding: 10px;
}
}
.chapter,
.volume,
.series,
.expected {
.overlay-information--centered {
div {
height: 32px;
width: 32px;
i {
font-size: 1.4rem;
line-height: 32px;
}
}
}
}

View file

@ -1,4 +1,4 @@
$scrollbarHeight: 34px;
$scrollbarHeight: 35px;
img {
user-select: none;
@ -9,29 +9,31 @@ img {
align-items: center;
&.full-width {
height: calc(var(--vh)*100);
height: 100dvh;
display: grid;
}
&.full-height {
height: calc(100vh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos
height: calc(100dvh); // We need to - $scrollbarHeight when there is a horizontal scroll on macos
display: flex;
align-content: center;
overflow-y: hidden;
}
&.original {
height: 100vh;
height: calc(100dvh);
display: grid;
}
.full-height {
width: auto;
margin: auto;
max-height: calc(var(--vh)*100);
overflow: hidden; // This technically will crop and make it just fit
max-height: calc(100dvh);
height: calc(100dvh);
vertical-align: top;
object-fit: cover;
&.wide {
height: 100vh;
height: calc(100dvh);
}
}
@ -46,12 +48,13 @@ img {
width: 100%;
margin: 0 auto;
vertical-align: top;
max-width: fit-content;
object-fit: contain;
width: 100%;
}
.fit-to-screen.full-width {
width: 100%;
max-height: calc(var(--vh)*100);
max-height: calc(100dvh);
}
}

View file

@ -0,0 +1,209 @@
@use './theme/variables' as theme;
.title {
color: white;
font-weight: bold;
font-size: 1.75rem;
}
.image-container {
align-self: flex-start;
max-height: 400px;
max-width: 280px;
}
.subtitle {
color: lightgrey;
font-weight: bold;
font-size: 0.8rem;
}
.main-container {
overflow: unset !important;
margin-top: 15px;
}
::ng-deep .badge-expander .content a {
font-size: 0.8rem;
}
.btn-group > .btn.dropdown-toggle-split:not(first-child){
border-top-right-radius: var(--bs-border-radius) !important;
border-bottom-right-radius: var(--bs-border-radius) !important;
border-width: 1px 1px 1px 0 !important;
}
.btn-group > .btn:not(:last-child):not(.dropdown-toggle) {
border-width: 1px 0 1px 1px !important;
}
.card-body > div:nth-child(2) {
height: 50px;
overflow: hidden;
-webkit-line-clamp: 2;
display: -webkit-box;
overflow: hidden;
-webkit-box-orient: vertical;
}
.under-image ~ .overlay-information {
top: -404px;
height: 364px;
}
.overlay-information {
position: relative;
top: -364px;
height: 364px;
transition: all 0.2s;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
&:hover {
cursor: pointer;
background-color: var(--card-overlay-hover-bg-color) !important;
.overlay-information--centered {
visibility: visible;
}
}
.overlay-information--centered {
position: absolute;
border-radius: 15px;
background-color: rgba(0, 0, 0, .7);
border-radius: 50px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 115;
visibility: hidden;
&:hover {
background-color: var(--primary-color) !important;
cursor: pointer;
}
div {
width: 60px;
height: 60px;
i {
font-size: 1.6rem;
line-height: 60px;
width: 100%;
}
}
}
}
.progress {
border-radius: 0;
}
.progress-banner.series {
position: relative;
}
::ng-deep .progress-banner.series span {
position: absolute;
left: 50%;
transform: translate(-50%, -50%);
color: white;
top: 50%;
}
.carousel-tabs-container {
overflow-x: auto;
white-space: nowrap;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
scrollbar-width: none;
box-shadow: inset -1px -2px 0px -1px var(--elevation-layer9);
}
.carousel-tabs-container::-webkit-scrollbar {
display: none;
}
.nav-tabs {
flex-wrap: nowrap;
}
.upper-details {
font-size: 0.9rem;
}
::ng-deep .carousel-container .header i.fa-plus, ::ng-deep .carousel-container .header i.fa-pen{
border-width: 1px;
border-style: solid;
border-radius: 5px;
border-color: var(--primary-color);
padding: 5px;
vertical-align: middle;
&:hover {
background-color: var(--primary-color-dark-shade);
}
}
::ng-deep .image-container.mobile-bg app-image img {
max-height: 400px;
object-fit: contain;
}
@media (max-width: theme.$grid-breakpoints-lg) {
.carousel-tabs-container {
mask-image: linear-gradient(transparent, black 0%, black 90%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
}
}
::ng-deep .image-container.mobile-bg app-image img {
max-height: 100dvh !important;
object-fit: cover !important;
}
/* col-lg */
@media (max-width: theme.$grid-breakpoints-lg) {
.image-container.mobile-bg{
width: 100vw;
top: calc(var(--nav-offset) - 20px);
left: 0;
pointer-events: none;
position: fixed !important;
display: block !important;
max-height: unset !important;
max-width: unset !important;
height: 100dvh !important;
}
::ng-deep .image-container.mobile-bg app-image img {
max-height: unset !important;
opacity: 0.05 !important;
filter: blur(5px) !important;
max-width: 100dvw;
height: 100dvh !important;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
object-fit: cover;
}
.progress-banner {
display:none;
}
.under-image {
display: none;
}
}
.upper-details {
font-size: 0.9rem;
}
@media (max-width: theme.$grid-breakpoints-lg) {
.carousel-tabs-container {
mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
-webkit-mask-image: linear-gradient(to right, transparent, black 0%, black 90%, transparent 100%);
}
}

View file

@ -0,0 +1,36 @@
import {Directive, EventEmitter, HostListener, Output} from '@angular/core';
@Directive({
selector: '[appDblClick]',
standalone: true
})
export class DblClickDirective {
@Output() singleClick = new EventEmitter<Event>();
@Output() doubleClick = new EventEmitter<Event>();
private lastTapTime = 0;
private tapTimeout = 300; // Time threshold for a double tap (in milliseconds)
private singleClickTimeout: any;
@HostListener('click', ['$event'])
handleClick(event: Event): void {
const currentTime = new Date().getTime();
if (currentTime - this.lastTapTime < this.tapTimeout) {
// Detected a double click/tap
clearTimeout(this.singleClickTimeout); // Prevent single-click emission
event.stopPropagation();
event.preventDefault();
this.doubleClick.emit(event);
} else {
// Delay single-click emission to check if a double-click occurs
this.singleClickTimeout = setTimeout(() => {
this.singleClick.emit(event); // Optional: emit single-click if no double-click follows
}, this.tapTimeout);
}
this.lastTapTime = currentTime;
}
}

View file

@ -0,0 +1,13 @@
import { Directive, HostListener } from '@angular/core';
@Directive({
selector: '[appEnterBlur]',
standalone: true,
})
export class EnterBlurDirective {
@HostListener('keydown.enter', ['$event'])
onEnter(event: KeyboardEvent): void {
event.preventDefault();
document.body.click();
}
}

View file

@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
@Injectable({
providedIn: 'root'

View file

@ -4,7 +4,7 @@ import { ToastrService } from 'ngx-toastr';
import { Observable } from 'rxjs';
import { map, take } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
@Injectable({
providedIn: 'root'

View file

@ -0,0 +1,62 @@
export const isSafari = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
].includes(navigator.platform)
// iPad on iOS 13 detection
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document);
/**
* Represents a Version for a browser
*/
export class Version {
major: number;
minor: number;
patch: number;
constructor(major: number, minor: number, patch: number) {
this.major = major;
this.minor = minor;
this.patch = patch;
}
isLessThan(other: Version): boolean {
if (this.major < other.major) return true;
if (this.major > other.major) return false;
if (this.minor < other.minor) return true;
if (this.minor > other.minor) return false;
return this.patch < other.patch;
}
isGreaterThan(other: Version): boolean {
if (this.major > other.major) return true;
if (this.major < other.major) return false;
if (this.minor > other.minor) return true;
if (this.minor < other.minor) return false;
return this.patch > other.patch;
}
isEqualTo(other: Version): boolean {
return (
this.major === other.major &&
this.minor === other.minor &&
this.patch === other.patch
);
}
}
export const getIosVersion = () => {
const match = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
if (match) {
const major = parseInt(match[1], 10);
const minor = parseInt(match[2], 10);
const patch = parseInt(match[3] || '0', 10);
return new Version(major, minor, patch);
}
return null;
}

View file

@ -1,16 +1,11 @@
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { Router } from '@angular/router';
import { ToastrService } from 'ngx-toastr';
import { catchError } from 'rxjs/operators';
import { AccountService } from '../_services/account.service';
import {translate, TranslocoService} from "@ngneat/transloco";
import {translate, TranslocoService} from "@jsverse/transloco";
@Injectable()
export class ErrorInterceptor implements HttpInterceptor {

View file

@ -1,10 +1,5 @@
import {Injectable} from '@angular/core';
import {
HttpRequest,
HttpHandler,
HttpEvent,
HttpInterceptor
} from '@angular/common/http';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import {Observable, switchMap} from 'rxjs';
import { AccountService } from '../_services/account.service';
import { take } from 'rxjs/operators';

View file

@ -1,16 +1,16 @@
import { AgeRestriction } from '../metadata/age-restriction';
import { Library } from '../library/library';
import {AgeRestriction} from '../metadata/age-restriction';
import {Library} from '../library/library';
export interface Member {
id: number;
username: string;
email: string;
lastActive: string; // datetime
lastActiveUtc: string; // datetime
created: string; // datetime
createdUtc: string; // datetime
roles: string[];
libraries: Library[];
ageRestriction: AgeRestriction;
isPending: boolean;
id: number;
username: string;
email: string;
lastActive: string; // datetime
lastActiveUtc: string; // datetime
created: string; // datetime
createdUtc: string; // datetime
roles: string[];
libraries: Library[];
ageRestriction: AgeRestriction;
isPending: boolean;
}

View file

@ -1,13 +1,29 @@
import { MangaFile } from './manga-file';
import { AgeRating } from './metadata/age-rating';
import {PublicationStatus} from "./metadata/publication-status";
import {Genre} from "./metadata/genre";
import {Tag} from "./tag";
import {Person} from "./metadata/person";
import {IHasCast} from "./common/i-has-cast";
import {IHasReadingTime} from "./common/i-has-reading-time";
import {IHasCover} from "./common/i-has-cover";
import {IHasProgress} from "./common/i-has-progress";
export const LooseLeafOrDefaultNumber = -100000;
export const SpecialVolumeNumber = 100000;
/**
* Chapter table object. This does not have metadata on it, use ChapterMetadata which is the same Chapter but with those fields.
*/
export interface Chapter {
export interface Chapter extends IHasCast, IHasReadingTime, IHasCover, IHasProgress {
id: number;
range: string;
/**
* @deprecated Use minNumber/maxNumber
*/
number: string;
minNumber: number;
maxNumber: number;
files: Array<MangaFile>;
/**
* This is used in the UI, it is not updated or sent to Backend
@ -42,4 +58,53 @@ export interface Chapter {
webLinks: string;
isbn: string;
lastReadingProgress: string;
sortOrder: number;
primaryColor: string;
secondaryColor: string;
year: string;
language: string;
publicationStatus: PublicationStatus;
count: number;
totalCount: number;
genres: Array<Genre>;
tags: Array<Tag>;
writers: Array<Person>;
coverArtists: Array<Person>;
publishers: Array<Person>;
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
imprints: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
translators: Array<Person>;
teams: Array<Person>;
locations: Array<Person>;
summaryLocked: boolean;
genresLocked: boolean;
tagsLocked: boolean;
writerLocked: boolean;
coverArtistLocked: boolean;
publisherLocked: boolean;
characterLocked: boolean;
pencillerLocked: boolean;
inkerLocked: boolean;
imprintLocked: boolean;
coloristLocked: boolean;
lettererLocked: boolean;
editorLocked: boolean;
translatorLocked: boolean;
teamLocked: boolean;
locationLocked: boolean;
ageRatingLocked: boolean;
languageLocked: boolean;
isbnLocked: boolean;
titleNameLocked: boolean;
sortOrderLocked: boolean;
releaseDateLocked: boolean;
}

View file

@ -1,11 +1,25 @@
export interface CollectionTag {
id: number;
title: string;
promoted: boolean;
/**
* This is used as a placeholder to store the coverImage url. The backend does not use this or send it.
*/
coverImage: string;
coverImageLocked: boolean;
summary: string;
}
import {ScrobbleProvider} from "../_services/scrobbling.service";
import {AgeRating} from "./metadata/age-rating";
export interface UserCollection {
id: number;
title: string;
promoted: boolean;
/**
* This is used as a placeholder to store the coverImage url. The backend does not use this or send it.
*/
coverImage: string;
coverImageLocked: boolean;
summary: string;
lastSyncUtc: string;
owner: string;
source: ScrobbleProvider;
sourceUrl: string | null;
totalSourceCount: number;
/**
* HTML anchors separated by <br/>
*/
missingSeriesFromSource: string | null;
ageRating: AgeRating;
itemCount: number;
}

View file

@ -0,0 +1,8 @@
export interface MalStack {
title: string;
stackId: number;
url: string;
author?: string;
seriesCount: number;
restackCount: number;
}

View file

@ -0,0 +1,50 @@
import {Person} from "../metadata/person";
export interface IHasCast {
writerLocked: boolean;
coverArtistLocked: boolean;
publisherLocked: boolean;
characterLocked: boolean;
pencillerLocked: boolean;
inkerLocked: boolean;
imprintLocked: boolean;
coloristLocked: boolean;
lettererLocked: boolean;
editorLocked: boolean;
translatorLocked: boolean;
teamLocked: boolean;
locationLocked: boolean;
languageLocked: boolean;
writers: Array<Person>;
coverArtists: Array<Person>;
publishers: Array<Person>;
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
imprints: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
translators: Array<Person>;
teams: Array<Person>;
locations: Array<Person>;
}
export function hasAnyCast(entity: IHasCast | null | undefined): boolean {
if (entity === null || entity === undefined) return false;
return entity.writers.length > 0 ||
entity.coverArtists.length > 0 ||
entity.publishers.length > 0 ||
entity.characters.length > 0 ||
entity.pencillers.length > 0 ||
entity.inkers.length > 0 ||
entity.imprints.length > 0 ||
entity.colorists.length > 0 ||
entity.letterers.length > 0 ||
entity.editors.length > 0 ||
entity.translators.length > 0 ||
entity.teams.length > 0 ||
entity.locations.length > 0;
}

View file

@ -0,0 +1,5 @@
export interface IHasCover {
coverImage?: string;
primaryColor: string;
secondaryColor: string;
}

View file

@ -0,0 +1,4 @@
export interface IHasProgress {
pages: number;
pagesRead: number;
}

View file

@ -0,0 +1,8 @@
export interface IHasReadingTime {
minHoursToRead: number;
maxHoursToRead: number;
avgHoursToRead: number;
pages: number;
wordCount: number;
}

View file

@ -0,0 +1 @@
export const DefaultModalOptions = {scrollable: true, size: 'xl', fullscreen: 'xl'};

View file

@ -0,0 +1,7 @@
export interface EmailHistory {
sent: boolean;
sendDate: string;
emailTemplate: string;
errorMessage: string;
toUserName: string;
}

View file

@ -0,0 +1,4 @@
export interface ChapterRemovedEvent {
chapterId: number;
seriesId: number;
}

View file

@ -0,0 +1,3 @@
export interface SiteThemeUpdatedEvent {
themeName: string;
}

View file

@ -1,12 +1,26 @@
export interface UpdateVersionEvent {
currentVersion: string;
updateVersion: string;
updateBody: string;
updateTitle: string;
updateUrl: string;
isDocker: boolean;
publishDate: string;
isOnNightlyInRelease: boolean;
isReleaseNewer: boolean;
isReleaseEqual: boolean;
currentVersion: string;
updateVersion: string;
updateBody: string;
updateTitle: string;
updateUrl: string;
isDocker: boolean;
publishDate: string;
isOnNightlyInRelease: boolean;
isReleaseNewer: boolean;
isReleaseEqual: boolean;
added: Array<string>;
removed: Array<string>;
changed: Array<string>;
fixed: Array<string>;
theme: Array<string>;
developer: Array<string>;
api: Array<string>;
featureRequests: Array<string>;
knownIssues: Array<string>;
/**
* The part above the changelog part
*/
blogPart: string;
}

View file

@ -0,0 +1,4 @@
export interface VolumeRemovedEvent {
volumeId: number;
seriesId: number;
}

View file

@ -0,0 +1,9 @@
export interface LicenseInfo {
expirationDate: string;
isActive: boolean;
isCancelled: boolean;
isValidVersion: boolean;
registeredEmail: string;
totalMonthsSubbed: number;
hasLicense: boolean;
}

View file

@ -0,0 +1,6 @@
import {MatchStateOption} from "./match-state-option";
export interface ManageMatchFilter {
matchStateOption: MatchStateOption;
searchTerm: string;
}

View file

@ -0,0 +1,7 @@
import {Series} from "../series";
export interface ManageMatchSeries {
series: Series;
isMatched: boolean;
validUntilUtc: string;
}

View file

@ -0,0 +1,11 @@
export enum MatchStateOption {
All = 0,
Matched = 1,
NotMatched = 2,
Error = 3,
DontMatch = 4
}
export const allMatchStates = [
MatchStateOption.Matched, MatchStateOption.NotMatched, MatchStateOption.Error, MatchStateOption.DontMatch
];

View file

@ -0,0 +1,8 @@
export interface UserTokenInfo {
userId: number;
username: string;
isAniListTokenSet: boolean;
aniListValidUntilUtc: string;
isAniListTokenValid: boolean;
isMalTokenSet: boolean;
}

View file

@ -6,9 +6,12 @@ export enum LibraryType {
Book = 2,
Images = 3,
LightNovel = 4,
Magazine = 5
ComicVine = 5,
Magazine = 6
}
export const allLibraryTypes = [LibraryType.Manga, LibraryType.ComicVine, LibraryType.Comic, LibraryType.Book, LibraryType.LightNovel, LibraryType.Images, LibraryType.Magazine];
export interface Library {
id: number;
name: string;
@ -23,6 +26,7 @@ export interface Library {
manageCollections: boolean;
manageReadingLists: boolean;
allowScrobbling: boolean;
allowMetadataMatching: boolean;
collapseSeriesRelationships: boolean;
libraryFileTypes: Array<FileTypeGroup>;
excludePatterns: Array<string>;

View file

@ -1,39 +0,0 @@
import { Genre } from "./genre";
import { AgeRating } from "./age-rating";
import { PublicationStatus } from "./publication-status";
import { Person } from "./person";
import { Tag } from "../tag";
export interface ChapterMetadata {
id: number;
chapterId: number;
title: string;
year: string;
ageRating: AgeRating;
releaseDate: string;
language: string;
publicationStatus: PublicationStatus;
summary: string;
count: number;
totalCount: number;
wordCount: number;
genres: Array<Genre>;
tags: Array<Tag>;
writers: Array<Person>;
coverArtists: Array<Person>;
publishers: Array<Person>;
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
translators: Array<Person>;
}

View file

@ -3,3 +3,10 @@ export interface Language {
title: string;
}
export interface KavitaLocale {
fileName: string; // isoCode aka what maps to the file on disk and what transloco loads
renderName: string;
translationCompletion: number;
isRtL: boolean;
hash: string;
}

View file

@ -1,20 +1,33 @@
import {IHasCover} from "../common/i-has-cover";
export enum PersonRole {
Other = 1,
Artist = 2,
Writer = 3,
Penciller = 4,
Inker = 5,
Colorist = 6,
Letterer = 7,
CoverArtist = 8,
Editor = 9,
Publisher = 10,
Character = 11,
Translator = 12
Other = 1,
Artist = 2,
Writer = 3,
Penciller = 4,
Inker = 5,
Colorist = 6,
Letterer = 7,
CoverArtist = 8,
Editor = 9,
Publisher = 10,
Character = 11,
Translator = 12,
Imprint = 13,
Team = 14,
Location = 15
}
export interface Person {
id: number;
name: string;
role: PersonRole;
}
export interface Person extends IHasCover {
id: number;
name: string;
description: string;
coverImage?: string;
coverImageLocked: boolean;
malId?: number;
aniListId?: number;
hardcoverId?: string;
asin?: string;
primaryColor: string;
secondaryColor: string;
}

View file

@ -1,6 +1,5 @@
import { MangaFormat } from "../manga-format";
import { SeriesFilterV2 } from "./v2/series-filter-v2";
import {FilterField} from "./v2/filter-field";
import {MangaFormat} from "../manga-format";
import {SeriesFilterV2} from "./v2/series-filter-v2";
export interface FilterItem<T> {
title: string;
@ -24,7 +23,8 @@ export enum SortField {
/**
* Kavita+ only
*/
AverageRating = 8
AverageRating = 8,
Random = 9
}
export const allSortFields = Object.keys(SortField)
@ -33,22 +33,22 @@ export const allSortFields = Object.keys(SortField)
export const mangaFormatFilters = [
{
title: 'Images',
title: 'images',
value: MangaFormat.IMAGE,
selected: false
},
{
title: 'EPUB',
title: 'epub',
value: MangaFormat.EPUB,
selected: false
},
{
title: 'PDF',
title: 'pdf',
value: MangaFormat.PDF,
selected: false
},
{
title: 'ARCHIVE',
title: 'archive',
value: MangaFormat.ARCHIVE,
selected: false
}

View file

@ -1,18 +1,17 @@
import { CollectionTag } from "../collection-tag";
import { Genre } from "./genre";
import { AgeRating } from "./age-rating";
import { PublicationStatus } from "./publication-status";
import { Person } from "./person";
import { Tag } from "../tag";
import {IHasCast} from "../common/i-has-cast";
export interface SeriesMetadata {
export interface SeriesMetadata extends IHasCast {
seriesId: number;
summary: string;
totalCount: number;
maxCount: number;
collectionTags: Array<CollectionTag>;
genres: Array<Genre>;
tags: Array<Tag>;
writers: Array<Person>;
@ -21,10 +20,13 @@ export interface SeriesMetadata {
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
imprints: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
translators: Array<Person>;
teams: Array<Person>;
locations: Array<Person>;
ageRating: AgeRating;
releaseYear: number;
language: string;
@ -40,10 +42,13 @@ export interface SeriesMetadata {
characterLocked: boolean;
pencillerLocked: boolean;
inkerLocked: boolean;
imprintLocked: boolean;
coloristLocked: boolean;
lettererLocked: boolean;
editorLocked: boolean;
translatorLocked: boolean;
teamLocked: boolean;
locationLocked: boolean;
ageRatingLocked: boolean;
releaseYearLocked: boolean;
languageLocked: boolean;

View file

@ -43,4 +43,5 @@ export enum FilterComparison {
/// Is Date not between now and X seconds ago
/// </summary>
IsNotInLast = 15,
IsEmpty = 16
}

View file

@ -1,3 +1,5 @@
import {PersonRole} from "../person";
export enum FilterField
{
None = -1,
@ -29,7 +31,11 @@ export enum FilterField
FilePath = 25,
WantToRead = 26,
ReadingDate = 27,
AverageRating = 28
AverageRating = 28,
Imprint = 29,
Team = 30,
Location = 31,
ReadLast = 32
}
@ -44,3 +50,36 @@ enumArray.sort((a, b) => a.value.localeCompare(b.value));
export const allFields = enumArray
.map(key => parseInt(key.key, 10))as FilterField[];
export const allPeople = [
FilterField.Characters,
FilterField.Colorist,
FilterField.CoverArtist,
FilterField.Editor,
FilterField.Inker,
FilterField.Letterer,
FilterField.Penciller,
FilterField.Publisher,
FilterField.Translators,
FilterField.Writers,
];
export const personRoleForFilterField = (role: PersonRole) => {
switch (role) {
case PersonRole.Artist: return FilterField.CoverArtist;
case PersonRole.Character: return FilterField.Characters;
case PersonRole.Colorist: return FilterField.Colorist;
case PersonRole.CoverArtist: return FilterField.CoverArtist;
case PersonRole.Editor: return FilterField.Editor;
case PersonRole.Inker: return FilterField.Inker;
case PersonRole.Letterer: return FilterField.Letterer;
case PersonRole.Penciller: return FilterField.Penciller;
case PersonRole.Publisher: return FilterField.Publisher;
case PersonRole.Translator: return FilterField.Translators;
case PersonRole.Writer: return FilterField.Writers;
case PersonRole.Imprint: return FilterField.Imprint;
case PersonRole.Location: return FilterField.Location;
case PersonRole.Team: return FilterField.Team;
case PersonRole.Other: return FilterField.None;
}
};

View file

@ -0,0 +1,7 @@
export enum QueryContext
{
None = 1,
Search = 2,
Recommended = 3,
Dashboard = 4,
}

View file

@ -0,0 +1,6 @@
import {Person} from "../metadata/person";
export interface BrowsePerson extends Person {
seriesCount: number;
issueCount: number;
}

View file

@ -0,0 +1,6 @@
export enum PdfLayoutMode {
Multiple = 0,
Single = 1,
Book = 2,
InfiniteScroll = 3
}

View file

@ -0,0 +1,6 @@
export enum PdfScrollMode {
Vertical = 0,
Horizontal = 1,
Wrapped = 2,
Page = 3
}

View file

@ -0,0 +1,5 @@
export enum PdfSpreadMode {
None = 0,
Odd = 1,
Even = 2
}

View file

@ -0,0 +1,4 @@
export enum PdfTheme{
Dark = 0,
Light = 1
}

View file

@ -1,48 +1,61 @@
import { LayoutMode } from 'src/app/manga-reader/_models/layout-mode';
import { BookPageLayoutMode } from '../readers/book-page-layout-mode';
import { PageLayoutMode } from '../page-layout-mode';
import { PageSplitOption } from './page-split-option';
import { ReaderMode } from './reader-mode';
import { ReadingDirection } from './reading-direction';
import { ScalingOption } from './scaling-option';
import { SiteTheme } from './site-theme';
import {LayoutMode} from 'src/app/manga-reader/_models/layout-mode';
import {BookPageLayoutMode} from '../readers/book-page-layout-mode';
import {PageLayoutMode} from '../page-layout-mode';
import {PageSplitOption} from './page-split-option';
import {ReaderMode} from './reader-mode';
import {ReadingDirection} from './reading-direction';
import {ScalingOption} from './scaling-option';
import {SiteTheme} from './site-theme';
import {WritingStyle} from "./writing-style";
import {PdfTheme} from "./pdf-theme";
import {PdfScrollMode} from "./pdf-scroll-mode";
import {PdfLayoutMode} from "./pdf-layout-mode";
import {PdfSpreadMode} from "./pdf-spread-mode";
export interface Preferences {
// Manga Reader
readingDirection: ReadingDirection;
scalingOption: ScalingOption;
pageSplitOption: PageSplitOption;
readerMode: ReaderMode;
autoCloseMenu: boolean;
layoutMode: LayoutMode;
backgroundColor: string;
showScreenHints: boolean;
emulateBook: boolean;
swipeToPaginate: boolean;
// Manga Reader
readingDirection: ReadingDirection;
scalingOption: ScalingOption;
pageSplitOption: PageSplitOption;
readerMode: ReaderMode;
autoCloseMenu: boolean;
layoutMode: LayoutMode;
backgroundColor: string;
showScreenHints: boolean;
emulateBook: boolean;
swipeToPaginate: boolean;
allowAutomaticWebtoonReaderDetection: boolean;
// Book Reader
bookReaderMargin: number;
bookReaderLineSpacing: number;
bookReaderFontSize: number;
bookReaderFontFamily: string;
bookReaderTapToPaginate: boolean;
bookReaderReadingDirection: ReadingDirection;
bookReaderWritingStyle: WritingStyle;
bookReaderThemeName: string;
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
// Book Reader
bookReaderMargin: number;
bookReaderLineSpacing: number;
bookReaderFontSize: number;
bookReaderFontFamily: string;
bookReaderTapToPaginate: boolean;
bookReaderReadingDirection: ReadingDirection;
bookReaderWritingStyle: WritingStyle;
bookReaderThemeName: string;
bookReaderLayoutMode: BookPageLayoutMode;
bookReaderImmersiveMode: boolean;
// Global
theme: SiteTheme;
globalPageLayoutMode: PageLayoutMode;
blurUnreadSummaries: boolean;
promptForDownloadSize: boolean;
noTransitions: boolean;
collapseSeriesRelationships: boolean;
shareReviews: boolean;
locale: string;
// PDF Reader
pdfTheme: PdfTheme;
pdfScrollMode: PdfScrollMode;
pdfSpreadMode: PdfSpreadMode;
// Global
theme: SiteTheme;
globalPageLayoutMode: PageLayoutMode;
blurUnreadSummaries: boolean;
promptForDownloadSize: boolean;
noTransitions: boolean;
collapseSeriesRelationships: boolean;
shareReviews: boolean;
locale: string;
// Kavita+
aniListScrobblingEnabled: boolean;
wantToReadSync: boolean;
}
export const readingDirections = [{text: 'left-to-right', value: ReadingDirection.LeftToRight}, {text: 'right-to-left', value: ReadingDirection.RightToLeft}];
@ -50,6 +63,10 @@ export const bookWritingStyles = [{text: 'horizontal', value: WritingStyle.Horiz
export const scalingOptions = [{text: 'automatic', value: ScalingOption.Automatic}, {text: 'fit-to-height', value: ScalingOption.FitToHeight}, {text: 'fit-to-width', value: ScalingOption.FitToWidth}, {text: 'original', value: ScalingOption.Original}];
export const pageSplitOptions = [{text: 'fit-to-screen', value: PageSplitOption.FitSplit}, {text: 'right-to-left', value: PageSplitOption.SplitRightToLeft}, {text: 'left-to-right', value: PageSplitOption.SplitLeftToRight}, {text: 'no-split', value: PageSplitOption.NoSplit}];
export const readingModes = [{text: 'left-to-right', value: ReaderMode.LeftRight}, {text: 'up-to-down', value: ReaderMode.UpDown}, {text: 'webtoon', value: ReaderMode.Webtoon}];
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // , {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const layoutModes = [{text: 'single', value: LayoutMode.Single}, {text: 'double', value: LayoutMode.Double}, {text: 'double-manga', value: LayoutMode.DoubleReversed}]; // TODO: Build this, {text: 'Double (No Cover)', value: LayoutMode.DoubleNoCover}
export const bookLayoutModes = [{text: 'scroll', value: BookPageLayoutMode.Default}, {text: '1-column', value: BookPageLayoutMode.Column1}, {text: '2-column', value: BookPageLayoutMode.Column2}];
export const pageLayoutModes = [{text: 'cards', value: PageLayoutMode.Cards}, {text: 'list', value: PageLayoutMode.List}];
export const pdfLayoutModes = [{text: 'pdf-multiple', value: PdfLayoutMode.Multiple}, {text: 'pdf-book', value: PdfLayoutMode.Book}];
export const pdfScrollModes = [{text: 'pdf-vertical', value: PdfScrollMode.Vertical}, {text: 'pdf-horizontal', value: PdfScrollMode.Horizontal}, {text: 'pdf-page', value: PdfScrollMode.Page}];
export const pdfSpreadModes = [{text: 'pdf-none', value: PdfSpreadMode.None}, {text: 'pdf-odd', value: PdfSpreadMode.Odd}, {text: 'pdf-even', value: PdfSpreadMode.Even}];
export const pdfThemes = [{text: 'pdf-light', value: PdfTheme.Light}, {text: 'pdf-dark', value: PdfTheme.Dark}];

View file

@ -3,9 +3,9 @@
*/
export enum ThemeProvider {
System = 1,
User = 2
Custom = 2,
}
/**
* Theme for the whole instance
*/
@ -20,4 +20,8 @@
* The actual class the root is defined against. It is generated at the backend.
*/
selector: string;
}
description: string;
previewUrls: Array<string>;
author: string;
}

View file

@ -0,0 +1,11 @@
export interface FullProgress {
id: number;
chapterId: number;
pagesRead: number;
lastModified: string;
lastModifiedUtc: string;
created: string;
createdUtc: string;
appUserId: number;
userName: string;
}

View file

@ -1,37 +1,57 @@
import { LibraryType } from "./library/library";
import { MangaFormat } from "./manga-format";
import {LibraryType} from "./library/library";
import {MangaFormat} from "./manga-format";
import {IHasCover} from "./common/i-has-cover";
import {AgeRating} from "./metadata/age-rating";
import {IHasReadingTime} from "./common/i-has-reading-time";
import {IHasCast} from "./common/i-has-cast";
export interface ReadingListItem {
pagesRead: number;
pagesTotal: number;
seriesName: string;
seriesFormat: MangaFormat;
seriesId: number;
chapterId: number;
order: number;
chapterNumber: string;
volumeNumber: string;
libraryId: number;
id: number;
releaseDate: string;
title: string;
libraryType: LibraryType;
libraryName: string;
pagesRead: number;
pagesTotal: number;
seriesName: string;
seriesFormat: MangaFormat;
seriesId: number;
chapterId: number;
order: number;
chapterNumber: string;
volumeNumber: string;
libraryId: number;
id: number;
releaseDate: string;
title: string;
libraryType: LibraryType;
libraryName: string;
summary?: string;
}
export interface ReadingList {
id: number;
title: string;
summary: string;
promoted: boolean;
coverImageLocked: boolean;
items: Array<ReadingListItem>;
/**
* If this is empty or null, the cover image isn't set. Do not use this externally.
*/
coverImage: string;
startingYear: number;
startingMonth: number;
endingYear: number;
endingMonth: number;
export interface ReadingList extends IHasCover {
id: number;
title: string;
summary: string;
promoted: boolean;
coverImageLocked: boolean;
items: Array<ReadingListItem>;
/**
* If this is empty or null, the cover image isn't set. Do not use this externally.
*/
coverImage?: string;
primaryColor: string;
secondaryColor: string;
startingYear: number;
startingMonth: number;
endingYear: number;
endingMonth: number;
itemCount: number;
ageRating: AgeRating;
}
export interface ReadingListInfo extends IHasReadingTime, IHasReadingTime {
pages: number;
wordCount: number;
isAllEpub: boolean;
minHoursToRead: number;
maxHoursToRead: number;
avgHoursToRead: number;
}
export interface ReadingListCast extends IHasCast {}

View file

@ -4,7 +4,8 @@ export enum ScrobbleEventSortField {
LastModified = 2,
Type= 3,
Series = 4,
IsProcessed = 5
IsProcessed = 5,
ScrobbleEvent = 6
}
export interface ScrobbleEventFilter {

View file

@ -4,14 +4,18 @@ import { MangaFile } from "../manga-file";
import { SearchResult } from "./search-result";
import { Tag } from "../tag";
import {BookmarkSearchResult} from "./bookmark-search-result";
import {Genre} from "../metadata/genre";
import {ReadingList} from "../reading-list";
import {UserCollection} from "../collection-tag";
import {Person} from "../metadata/person";
export class SearchResultGroup {
libraries: Array<Library> = [];
series: Array<SearchResult> = [];
collections: Array<Tag> = [];
readingLists: Array<Tag> = [];
persons: Array<Tag> = [];
genres: Array<Tag> = [];
collections: Array<UserCollection> = [];
readingLists: Array<ReadingList> = [];
persons: Array<Person> = [];
genres: Array<Genre> = [];
tags: Array<Tag> = [];
files: Array<MangaFile> = [];
chapters: Array<Chapter> = [];

View file

@ -27,8 +27,9 @@ export interface MetadataTagDto {
export interface ExternalSeriesDetail {
name: string;
aniListId?: number;
malId?: number;
aniListId?: number | null;
malId?: number | null;
cbrId?: number | null;
synonyms: Array<string>;
plusMediaFormat: PlusMediaFormat;
siteUrl?: string;
@ -37,6 +38,11 @@ export interface ExternalSeriesDetail {
summary?: string;
volumeCount?: number;
chapterCount?: number;
/**
* These are duplicated with volumeCount based on where it's being invoked.
*/
volumes?: number;
chapters?: number;
staff: Array<SeriesStaff>;
tags: Array<MetadataTagDto>;
provider: ScrobbleProvider;

View file

@ -0,0 +1,6 @@
import {ExternalSeriesDetail} from "./external-series-detail";
export interface ExternalSeriesMatch {
series: ExternalSeriesDetail;
matchRating: number;
}

View file

@ -1,6 +1,5 @@
export interface HourEstimateRange{
export interface HourEstimateRange {
minHours: number;
maxHours: number;
avgHours: number;
//hasProgress: boolean;
}
}

View file

@ -15,4 +15,5 @@ export interface RelatedSeries {
doujinshis: Array<Series>;
parent: Array<Series>;
editions: Array<Series>;
annuals: Array<Series>;
}

View file

@ -14,7 +14,8 @@ export enum RelationKind {
* This is UI only. Backend will generate Parent series for everything but Prequel/Sequel
*/
Parent = 12,
Edition = 13
Edition = 13,
Annual = 14
}
const RelationKindsUnsorted = [
@ -22,6 +23,7 @@ const RelationKindsUnsorted = [
{text: 'Sequel', value: RelationKind.Sequel},
{text: 'Spin Off', value: RelationKind.SpinOff},
{text: 'Adaptation', value: RelationKind.Adaptation},
{text: 'Annual', value: RelationKind.Annual},
{text: 'Alternative Setting', value: RelationKind.AlternativeSetting},
{text: 'Alternative Version', value: RelationKind.AlternativeVersion},
{text: 'Side Story', value: RelationKind.SideStory},

View file

@ -1,67 +1,82 @@
import { MangaFormat } from './manga-format';
import { Volume } from './volume';
import {IHasCover} from "./common/i-has-cover";
import {IHasReadingTime} from "./common/i-has-reading-time";
import {IHasProgress} from "./common/i-has-progress";
export interface Series {
id: number;
name: string;
/**
* This is not shown to user
*/
originalName: string;
localizedName: string;
sortName: string;
coverImageLocked: boolean;
sortNameLocked: boolean;
localizedNameLocked: boolean;
nameLocked: boolean;
volumes: Volume[];
/**
* Total pages in series
*/
pages: number;
/**
* Total pages the logged in user has read
*/
pagesRead: number;
/**
* User's rating (0-5)
*/
userRating: number;
hasUserRated: boolean;
libraryId: number;
/**
* DateTime the entity was created
*/
created: string;
/**
* Format of the Series
*/
format: MangaFormat;
/**
* DateTime that represents last time the logged in user read this series
*/
latestReadDate: string;
/**
* DateTime representing last time a chapter was added to the Series
*/
lastChapterAdded: string;
/**
* DateTime representing last time the series folder was scanned
*/
lastFolderScanned: string;
/**
* Number of words in the series
*/
wordCount: number;
minHoursToRead: number;
maxHoursToRead: number;
avgHoursToRead: number;
/**
* Highest level folder containing this series
*/
folderPath: string;
export interface Series extends IHasCover, IHasReadingTime, IHasProgress {
id: number;
name: string;
/**
* This is not shown to user
*/
originalName: string;
localizedName: string;
sortName: string;
coverImageLocked: boolean;
sortNameLocked: boolean;
localizedNameLocked: boolean;
nameLocked: boolean;
volumes: Volume[];
/**
* Total pages in series
*/
pages: number;
/**
* Total pages the logged in user has read
*/
pagesRead: number;
/**
* User's rating (0-5)
*/
userRating: number;
hasUserRated: boolean;
libraryId: number;
/**
* DateTime the entity was created
*/
created: string;
/**
* Format of the Series
*/
format: MangaFormat;
/**
* DateTime that represents last time the logged in user read this series
*/
latestReadDate: string;
/**
* DateTime representing last time a chapter was added to the Series
*/
lastChapterAdded: string;
/**
* DateTime representing last time the series folder was scanned
*/
lastFolderScanned: string;
/**
* Number of words in the series
*/
wordCount: number;
minHoursToRead: number;
maxHoursToRead: number;
avgHoursToRead: number;
/**
* Highest level folder containing this series
*/
folderPath: string;
lowestFolderPath: string;
/**
* This is currently only used on Series detail page for recommendations
*/
summary?: string;
coverImage?: string;
primaryColor: string;
secondaryColor: string;
/**
* Kavita+ only. Will not perform any matching from Kavita+
*/
dontMatch: boolean;
/**
* Kavita+ only. Did this series not match and won't without manual match
*/
isBlacklisted: boolean;
}

View file

@ -7,4 +7,5 @@ export enum SideNavStreamType {
ExternalSource = 6,
AllSeries = 7,
WantToRead = 8,
BrowseAuthors = 9
}

View file

@ -1,5 +1,5 @@
import {SideNavStreamType} from "./sidenav-stream-type.enum";
import {Library, LibraryType} from "../library/library";
import {Library} from "../library/library";
import {CommonStream} from "../common-stream";
import {ExternalSource} from "./external-source";

View file

@ -0,0 +1,9 @@
import {Chapter} from "./chapter";
import {LibraryType} from "./library/library";
export interface StandaloneChapter extends Chapter {
seriesId: number;
libraryId: number;
libraryType: LibraryType;
volumeTitle?: string;
}

View file

@ -0,0 +1,4 @@
export interface ColorScape {
primary?: string;
secondary?: string;
}

View file

@ -0,0 +1,10 @@
export interface DownloadableSiteTheme {
name: string;
cssUrl: string;
previewUrls: Array<string>;
author: string;
isCompatible: boolean;
lastCompatibleVersion: string;
alreadyDownloaded: boolean;
description: string;
}

View file

@ -1,14 +1,16 @@
import { AgeRestriction } from './metadata/age-restriction';
import { Preferences } from './preferences/preferences';
import {AgeRestriction} from './metadata/age-restriction';
import {Preferences} from './preferences/preferences';
// This interface is only used for login and storing/retrieving JWT from local storage
export interface User {
username: string;
token: string;
refreshToken: string;
roles: string[];
preferences: Preferences;
apiKey: string;
email: string;
ageRestriction: AgeRestriction;
username: string;
token: string;
refreshToken: string;
roles: string[];
preferences: Preferences;
apiKey: string;
email: string;
ageRestriction: AgeRestriction;
hasRunScrobbleEventGeneration: boolean;
scrobbleEventGenerationRan: string; // datetime
}

View file

@ -1,7 +1,10 @@
import { Chapter } from './chapter';
import { HourEstimateRange } from './series-detail/hour-estimate-range';
import {IHasCover} from "./common/i-has-cover";
import {IHasReadingTime} from "./common/i-has-reading-time";
import {IHasProgress} from "./common/i-has-progress";
export interface Volume {
export interface Volume extends IHasCover, IHasReadingTime, IHasProgress {
id: number;
minNumber: number;
maxNumber: number;
@ -10,6 +13,7 @@ export interface Volume {
lastModifiedUtc: string;
pages: number;
pagesRead: number;
wordCount: number;
chapters: Array<Chapter>;
/**
* This is only available on the object when fetched for SeriesDetail
@ -18,4 +22,9 @@ export interface Volume {
minHoursToRead: number;
maxHoursToRead: number;
avgHoursToRead: number;
coverImage?: string;
coverImageLocked: boolean;
primaryColor: string;
secondaryColor: string;
}

View file

@ -0,0 +1,24 @@
export enum WikiLink {
Customize = 'https://wiki.kavitareader.com/guides/features/customization',
CustomizeExternalSource = 'https://wiki.kavitareader.com/guides/features/customization#external-source',
ReadingLists = 'https://wiki.kavitareader.com/guides/features/readinglists',
Collections = 'https://wiki.kavitareader.com/guides/features/collections',
SeriesRelationships = 'https://wiki.kavitareader.com/guides/features/relationships',
Bookmarks = 'https://wiki.kavitareader.com/guides/features/bookmarks',
DataCollection = 'https://wiki.kavitareader.com/troubleshooting/faq#q-does-kavita-collect-any-data-on-me',
MediaIssues = 'https://wiki.kavitareader.com/guides/admin-settings/mediaissues/',
KavitaPlusDiscordId = 'https://wiki.kavitareader.com/guides/admin-settings/kavita+#discord-id',
KavitaPlus = 'https://wiki.kavitareader.com/kavita+',
KavitaPlusFAQ = 'https://wiki.kavitareader.com/kavita+/faq',
ReadingListCBL = 'https://wiki.kavitareader.com/guides/features/readinglists#creating-a-reading-list-via-cbl',
Donation = 'https://wiki.kavitareader.com/donating',
Updating = 'https://wiki.kavitareader.com/guides/updating',
ManagingFiles = 'https://wiki.kavitareader.com/guides/scanner/managefiles',
Scanner = 'https://wiki.kavitareader.com/guides/scanner',
ScannerExclude = 'https://wiki.kavitareader.com/guides/admin-settings/libraries#exclude-patterns',
Library = 'https://wiki.kavitareader.com/guides/admin-settings/libraries',
UpdateNative = 'https://wiki.kavitareader.com/guides/updating/updating-native',
UpdateDocker = 'https://wiki.kavitareader.com/guides/updating/updating-docker',
OpdsClients = 'https://wiki.kavitareader.com/guides/features/opds/#opds-capable-clients',
Guides = 'https://wiki.kavitareader.com/guides'
}

View file

@ -1,22 +1,22 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import { Observable, of } from 'rxjs';
import { AgeRating } from '../_models/metadata/age-rating';
import { AgeRatingDto } from '../_models/metadata/age-rating-dto';
import {TranslocoService} from "@ngneat/transloco";
import {AgeRating} from '../_models/metadata/age-rating';
import {AgeRatingDto} from '../_models/metadata/age-rating-dto';
import {TranslocoService} from "@jsverse/transloco";
@Pipe({
name: 'ageRating',
standalone: true
standalone: true,
pure: true
})
export class AgeRatingPipe implements PipeTransform {
translocoService = inject(TranslocoService);
private readonly translocoService = inject(TranslocoService);
transform(value: AgeRating | AgeRatingDto | undefined): Observable<string> {
if (value === undefined || value === null) return of(this.translocoService.translate('age-rating-pipe.unknown') as string);
transform(value: AgeRating | AgeRatingDto | undefined): string {
if (value === undefined || value === null) return this.translocoService.translate('age-rating-pipe.unknown');
if (value.hasOwnProperty('title')) {
return of((value as AgeRatingDto).title);
return (value as AgeRatingDto).title;
}
switch (value) {
@ -54,7 +54,7 @@ export class AgeRatingPipe implements PipeTransform {
return this.translocoService.translate('age-rating-pipe.r18-plus');
}
return of(this.translocoService.translate('age-rating-pipe.unknown') as string);
return this.translocoService.translate('age-rating-pipe.unknown');
}
}

View file

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
import {translate} from "@jsverse/transloco";
import {BookPageLayoutMode} from "../_models/readers/book-page-layout-mode";
import {ScalingOption} from "../_models/preferences/scaling-option";
@Pipe({
name: 'bookPageLayoutMode',
standalone: true
})
export class BookPageLayoutModePipe implements PipeTransform {
transform(value: BookPageLayoutMode): string {
const v = parseInt(value + '', 10) as BookPageLayoutMode;
switch (v) {
case BookPageLayoutMode.Column1: return translate('preferences.1-column');
case BookPageLayoutMode.Column2: return translate('preferences.2-column');
case BookPageLayoutMode.Default: return translate('preferences.scroll');
}
}
}

View file

@ -1,7 +1,7 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import { CblBookResult } from 'src/app/_models/reading-list/cbl/cbl-book-result';
import { CblImportReason } from 'src/app/_models/reading-list/cbl/cbl-import-reason.enum';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
const failIcon = '<i aria-hidden="true" class="reading-list-fail--item fa-solid fa-circle-xmark me-1"></i>';
const successIcon = '<i aria-hidden="true" class="reading-list-success--item fa-solid fa-circle-check me-1"></i>';
@ -23,7 +23,7 @@ export class CblConflictReasonPipe implements PipeTransform {
case CblImportReason.EmptyFile:
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.empty-file');
case CblImportReason.NameConflict:
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.chapter-missing', {readingListName: result.readingListName});
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.name-conflict', {readingListName: result.readingListName});
case CblImportReason.SeriesCollision:
return failIcon + this.translocoService.translate('cbl-conflict-reason-pipe.series-collision', {seriesLink: `<a href="/library/${result.libraryId}/series/${result.seriesId}" target="_blank">${result.series}</a>`});
case CblImportReason.SeriesMissing:

View file

@ -1,6 +1,6 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import { CblImportResult } from 'src/app/_models/reading-list/cbl/cbl-import-result.enum';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
@Pipe({
name: 'cblImportResult',

View file

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import { translate } from '@jsverse/transloco';
@Pipe({
name: 'confirmTranslate',
standalone: true
})
export class ConfirmTranslatePipe implements PipeTransform {
transform(value: string | undefined | null): string | undefined | null {
if (!value) return value;
if (value.startsWith('confirm.')) {
return translate(value);
}
return value;
}
}

View file

@ -0,0 +1,25 @@
import {Pipe, PipeTransform} from '@angular/core';
import {CoverImageSize} from "../admin/_models/cover-image-size";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'coverImageSize',
standalone: true
})
export class CoverImageSizePipe implements PipeTransform {
transform(value: CoverImageSize): string {
switch (value) {
case CoverImageSize.Default:
return translate('cover-image-size.default');
case CoverImageSize.Medium:
return translate('cover-image-size.medium');
case CoverImageSize.Large:
return translate('cover-image-size.large');
case CoverImageSize.XLarge:
return translate('cover-image-size.xlarge');
}
}
}

View file

@ -1,6 +1,6 @@
import {Pipe, PipeTransform} from '@angular/core';
import { DayOfWeek } from 'src/app/_services/statistics.service';
import {translate} from "@ngneat/transloco";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'dayOfWeek',

View file

@ -1,5 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
@Pipe({
name: 'defaultDate',
@ -8,7 +8,6 @@ import {TranslocoService} from "@ngneat/transloco";
})
export class DefaultDatePipe implements PipeTransform {
// TODO: Figure out how to translate Never
constructor(private translocoService: TranslocoService) {
}
transform(value: any, replacementString = 'default-date-pipe.never'): string {

View file

@ -1,6 +1,6 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import { DevicePlatform } from 'src/app/_models/device/device-platform';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
@Pipe({
name: 'devicePlatform',

View file

@ -0,0 +1,21 @@
import {Pipe, PipeTransform} from '@angular/core';
import {EncodeFormat} from "../admin/_models/encode-format";
@Pipe({
name: 'encodeFormat',
standalone: true
})
export class EncodeFormatPipe implements PipeTransform {
transform(value: EncodeFormat): string {
switch (value) {
case EncodeFormat.PNG:
return 'PNG';
case EncodeFormat.WebP:
return 'WebP';
case EncodeFormat.AVIF:
return 'AVIF';
}
}
}

View file

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import {FileTypeGroup} from "../_models/library/file-type-group.enum";
import {translate} from "@ngneat/transloco";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'fileTypeGroup',

View file

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FilterComparison } from 'src/app/_models/metadata/v2/filter-comparison';
import {translate} from "@ngneat/transloco";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'filterComparison',
@ -42,6 +42,8 @@ export class FilterComparisonPipe implements PipeTransform {
return translate('filter-comparison-pipe.is-not-in-last');
case FilterComparison.MustContains:
return translate('filter-comparison-pipe.must-contains');
case FilterComparison.IsEmpty:
return translate('filter-comparison-pipe.is-empty');
default:
throw new Error(`Invalid FilterComparison value: ${value}`);
}

View file

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
import { FilterField } from 'src/app/_models/metadata/v2/filter-field';
import {translate} from "@ngneat/transloco";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'filterField',
@ -28,6 +28,12 @@ export class FilterFieldPipe implements PipeTransform {
return translate('filter-field-pipe.genres');
case FilterField.Inker:
return translate('filter-field-pipe.inker');
case FilterField.Imprint:
return translate('filter-field-pipe.imprint');
case FilterField.Team:
return translate('filter-field-pipe.team');
case FilterField.Location:
return translate('filter-field-pipe.location');
case FilterField.Languages:
return translate('filter-field-pipe.languages');
case FilterField.Libraries:
@ -66,6 +72,8 @@ export class FilterFieldPipe implements PipeTransform {
return translate('filter-field-pipe.want-to-read');
case FilterField.ReadingDate:
return translate('filter-field-pipe.read-date');
case FilterField.ReadLast:
return translate('filter-field-pipe.read-last');
case FilterField.AverageRating:
return translate('filter-field-pipe.average-rating');
default:

View file

@ -14,6 +14,6 @@ export class FilterPipe implements PipeTransform {
const ret = items.filter(item => callback(item));
if (ret.length === items.length) return items; // This will prevent a re-render
return ret;
}
}
}

View file

@ -9,16 +9,10 @@ import {shareReplay} from "rxjs/operators";
})
export class LanguageNamePipe implements PipeTransform {
constructor(private metadataService: MetadataService) {
}
constructor(private metadataService: MetadataService) {}
transform(isoCode: string): Observable<string> {
// TODO: See if we can speed this up. It rarely changes and is quite heavy to download on each page
return this.metadataService.getAllValidLanguages().pipe(map(lang => {
const l = lang.filter(l => l.isoCode === isoCode);
if (l.length > 0) return l[0].title;
return '';
}), shareReplay());
return this.metadataService.getLanguageNameForCode(isoCode).pipe(shareReplay());
}
}

View file

@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import {translate} from "@jsverse/transloco";
import {LayoutMode} from "../manga-reader/_models/layout-mode";
import {ScalingOption} from "../_models/preferences/scaling-option";
@Pipe({
name: 'layoutMode',
standalone: true
})
export class LayoutModePipe implements PipeTransform {
transform(value: LayoutMode): string {
const v = parseInt(value + '', 10) as LayoutMode;
switch (v) {
case LayoutMode.Single: return translate('preferences.single');
case LayoutMode.Double: return translate('preferences.double');
case LayoutMode.DoubleReversed: return translate('preferences.double-manga');
case LayoutMode.DoubleNoCover: return translate('preferences.double-no-cover');
}
}
}

View file

@ -0,0 +1,16 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import {LibraryService} from "../_services/library.service";
import {Observable} from "rxjs";
@Pipe({
name: 'libraryName',
standalone: true
})
export class LibraryNamePipe implements PipeTransform {
private readonly libraryService = inject(LibraryService);
transform(libraryId: number): Observable<string> {
return this.libraryService.getLibraryName(libraryId);
}
}

View file

@ -1,6 +1,6 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import { LibraryType } from '../_models/library/library';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
/**
* Returns the name of the LibraryType
@ -11,15 +11,21 @@ import {TranslocoService} from "@ngneat/transloco";
})
export class LibraryTypePipe implements PipeTransform {
translocoService = inject(TranslocoService);
private readonly translocoService = inject(TranslocoService);
transform(libraryType: LibraryType): string {
switch (libraryType) {
case LibraryType.Book:
return this.translocoService.translate('library-type-pipe.book');
case LibraryType.Comic:
return this.translocoService.translate('library-type-pipe.comic');
case LibraryType.ComicVine:
return this.translocoService.translate('library-type-pipe.comicVine');
case LibraryType.Images:
return this.translocoService.translate('library-type-pipe.image');
case LibraryType.Manga:
return this.translocoService.translate('library-type-pipe.manga');
case LibraryType.LightNovel:
return this.translocoService.translate('library-type-pipe.lightNovel');
case LibraryType.Magazine:
return this.translocoService.translate('library-type-pipe.magazine');
default:

View file

@ -0,0 +1,17 @@
import {Pipe, PipeTransform} from '@angular/core';
import {translate} from "@jsverse/transloco";
/**
* Transforms the log level string into a localized string
*/
@Pipe({
name: 'logLevel',
standalone: true,
pure: true
})
export class LogLevelPipe implements PipeTransform {
transform(value: string): string {
return translate('log-level-pipe.' + value.toLowerCase());
}
}

View file

@ -13,15 +13,15 @@ export class MangaFormatIconPipe implements PipeTransform {
transform(format: MangaFormat): string {
switch (format) {
case MangaFormat.EPUB:
return 'fa-book';
return 'fa fa-book';
case MangaFormat.ARCHIVE:
return 'fa-file-archive';
return 'fa-solid fa-file-zipper';
case MangaFormat.IMAGE:
return 'fa-image';
return 'fa-solid fa-file-image';
case MangaFormat.PDF:
return 'fa-file-pdf';
return 'fa-solid fa-file-pdf';
case MangaFormat.UNKNOWN:
return 'fa-question';
return 'fa-solid fa-file-circle-question';
}
}

View file

@ -1,6 +1,6 @@
import {Pipe, PipeTransform} from '@angular/core';
import { MangaFormat } from '../_models/manga-format';
import {TranslocoService} from "@ngneat/transloco";
import {TranslocoService} from "@jsverse/transloco";
/**
* Returns the string name for the format

View file

@ -0,0 +1,27 @@
import { Pipe, PipeTransform } from '@angular/core';
import {MatchStateOption} from "../_models/kavitaplus/match-state-option";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'matchStateOption',
standalone: true
})
export class MatchStateOptionPipe implements PipeTransform {
transform(value: MatchStateOption): string {
switch (value) {
case MatchStateOption.DontMatch:
return translate('manage-matched-metadata.dont-match-label');
case MatchStateOption.All:
return translate('manage-matched-metadata.all-status-label');
case MatchStateOption.Matched:
return translate('manage-matched-metadata.matched-status-label');
case MatchStateOption.NotMatched:
return translate('manage-matched-metadata.unmatched-status-label');
case MatchStateOption.Error:
return translate('manage-matched-metadata.blacklist-status-label');
}
}
}

View file

@ -0,0 +1,45 @@
import {Pipe, PipeTransform} from '@angular/core';
import {MetadataSettingField} from "../admin/_models/metadata-setting-field";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'metadataSettingFiled',
standalone: true
})
export class MetadataSettingFiledPipe implements PipeTransform {
transform(value: MetadataSettingField): string {
switch (value) {
case MetadataSettingField.ChapterTitle:
return translate('metadata-setting-field-pipe.chapter-title');
case MetadataSettingField.ChapterSummary:
return translate('metadata-setting-field-pipe.chapter-summary');
case MetadataSettingField.ChapterReleaseDate:
return translate('metadata-setting-field-pipe.chapter-release-date');
case MetadataSettingField.ChapterPublisher:
return translate('metadata-setting-field-pipe.chapter-publisher');
case MetadataSettingField.ChapterCovers:
return translate('metadata-setting-field-pipe.chapter-covers');
case MetadataSettingField.AgeRating:
return translate('metadata-setting-field-pipe.age-rating');
case MetadataSettingField.People:
return translate('metadata-setting-field-pipe.people');
case MetadataSettingField.Covers:
return translate('metadata-setting-field-pipe.covers');
case MetadataSettingField.Summary:
return translate('metadata-setting-field-pipe.summary');
case MetadataSettingField.PublicationStatus:
return translate('metadata-setting-field-pipe.publication-status');
case MetadataSettingField.StartDate:
return translate('metadata-setting-field-pipe.start-date');
case MetadataSettingField.Genres:
return translate('metadata-setting-field-pipe.genres');
case MetadataSettingField.Tags:
return translate('metadata-setting-field-pipe.tags');
case MetadataSettingField.LocalizedName:
return translate('metadata-setting-field-pipe.localized-name');
}
}
}

View file

@ -0,0 +1,18 @@
import { Pipe, PipeTransform } from '@angular/core';
import {PageLayoutMode} from "../_models/page-layout-mode";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'pageLayoutMode',
standalone: true
})
export class PageLayoutModePipe implements PipeTransform {
transform(value: PageLayoutMode): string {
switch (value) {
case PageLayoutMode.Cards: return translate('preferences.cards');
case PageLayoutMode.List: return translate('preferences.list');
}
}
}

View file

@ -0,0 +1,22 @@
import { Pipe, PipeTransform } from '@angular/core';
import {translate} from "@jsverse/transloco";
import {PageSplitOption} from "../_models/preferences/page-split-option";
import {ScalingOption} from "../_models/preferences/scaling-option";
@Pipe({
name: 'pageSplitOption',
standalone: true
})
export class PageSplitOptionPipe implements PipeTransform {
transform(value: PageSplitOption): string {
const v = parseInt(value + '', 10) as PageSplitOption;
switch (v) {
case PageSplitOption.FitSplit: return translate('preferences.fit-to-screen');
case PageSplitOption.NoSplit: return translate('preferences.no-split');
case PageSplitOption.SplitLeftToRight: return translate('preferences.split-left-to-right');
case PageSplitOption.SplitRightToLeft: return translate('preferences.split-right-to-left');
}
}
}

View file

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
import {translate} from "@jsverse/transloco";
import {PdfScrollMode} from "../_models/preferences/pdf-scroll-mode";
@Pipe({
name: 'pdfScrollMode',
standalone: true
})
export class PdfScrollModePipe implements PipeTransform {
transform(value: PdfScrollMode): string {
const v = parseInt(value + '', 10) as PdfScrollMode;
switch (v) {
case PdfScrollMode.Wrapped: return translate('preferences.pdf-multiple');
case PdfScrollMode.Page: return translate('preferences.pdf-page');
case PdfScrollMode.Horizontal: return translate('preferences.pdf-horizontal');
case PdfScrollMode.Vertical: return translate('preferences.pdf-vertical');
}
}
}

View file

@ -0,0 +1,21 @@
import { Pipe, PipeTransform } from '@angular/core';
import {PdfSpreadMode} from "../_models/preferences/pdf-spread-mode";
import {translate} from "@jsverse/transloco";
import {ScalingOption} from "../_models/preferences/scaling-option";
@Pipe({
name: 'pdfSpreadMode',
standalone: true
})
export class PdfSpreadModePipe implements PipeTransform {
transform(value: PdfSpreadMode): string {
const v = parseInt(value + '', 10) as PdfSpreadMode;
switch (v) {
case PdfSpreadMode.None: return translate('preferences.pdf-none');
case PdfSpreadMode.Odd: return translate('preferences.pdf-odd');
case PdfSpreadMode.Even: return translate('preferences.pdf-even');
}
}
}

View file

@ -0,0 +1,20 @@
import { Pipe, PipeTransform } from '@angular/core';
import {PdfTheme} from "../_models/preferences/pdf-theme";
import {translate} from "@jsverse/transloco";
import {ScalingOption} from "../_models/preferences/scaling-option";
@Pipe({
name: 'pdfTheme',
standalone: true
})
export class PdfThemePipe implements PipeTransform {
transform(value: PdfTheme): string {
const v = parseInt(value + '', 10) as PdfTheme;
switch (v) {
case PdfTheme.Dark: return translate('preferences.pdf-dark');
case PdfTheme.Light: return translate('preferences.pdf-light');
}
}
}

View file

@ -1,6 +1,6 @@
import {inject, Pipe, PipeTransform} from '@angular/core';
import { PersonRole } from '../_models/metadata/person';
import {TranslocoService} from "@ngneat/transloco";
import {translate, TranslocoService} from "@jsverse/transloco";
@Pipe({
name: 'personRole',
@ -8,31 +8,38 @@ import {TranslocoService} from "@ngneat/transloco";
})
export class PersonRolePipe implements PipeTransform {
translocoService = inject(TranslocoService);
transform(value: PersonRole): string {
switch (value) {
case PersonRole.Artist:
return this.translocoService.translate('person-role-pipe.artist');
return translate('person-role-pipe.artist');
case PersonRole.Character:
return this.translocoService.translate('person-role-pipe.character');
return translate('person-role-pipe.character');
case PersonRole.Colorist:
return this.translocoService.translate('person-role-pipe.colorist');
return translate('person-role-pipe.colorist');
case PersonRole.CoverArtist:
return this.translocoService.translate('person-role-pipe.cover-artist');
return translate('person-role-pipe.artist');
case PersonRole.Editor:
return this.translocoService.translate('person-role-pipe.editor');
return translate('person-role-pipe.editor');
case PersonRole.Inker:
return this.translocoService.translate('person-role-pipe.inker');
return translate('person-role-pipe.inker');
case PersonRole.Letterer:
return this.translocoService.translate('person-role-pipe.letterer');
return translate('person-role-pipe.letterer');
case PersonRole.Penciller:
return this.translocoService.translate('person-role-pipe.penciller');
return translate('person-role-pipe.penciller');
case PersonRole.Publisher:
return this.translocoService.translate('person-role-pipe.publisher');
return translate('person-role-pipe.publisher');
case PersonRole.Imprint:
return translate('person-role-pipe.imprint');
case PersonRole.Writer:
return this.translocoService.translate('person-role-pipe.writer');
return translate('person-role-pipe.writer');
case PersonRole.Team:
return translate('person-role-pipe.team');
case PersonRole.Location:
return translate('person-role-pipe.location');
case PersonRole.Translator:
return translate('person-role-pipe.translator');
case PersonRole.Other:
return this.translocoService.translate('person-role-pipe.other');
return translate('person-role-pipe.other');
default:
return '';
}

View file

@ -0,0 +1,25 @@
import {Pipe, PipeTransform} from '@angular/core';
import {PlusMediaFormat} from "../_models/series-detail/external-series-detail";
import {translate} from "@jsverse/transloco";
@Pipe({
name: 'plusMediaFormat',
standalone: true
})
export class PlusMediaFormatPipe implements PipeTransform {
transform(value: PlusMediaFormat): string {
switch (value) {
case PlusMediaFormat.Manga:
return translate('library-type-pipe.manga');
case PlusMediaFormat.Comic:
return translate('library-type-pipe.comicVine');
case PlusMediaFormat.LightNovel:
return translate('library-type-pipe.lightNovel');
case PlusMediaFormat.Book:
return translate('library-type-pipe.book');
}
}
}

Some files were not shown because too many files have changed in this diff Show more