Merged develop in
This commit is contained in:
commit
d12a79892f
1443 changed files with 215765 additions and 44113 deletions
|
|
@ -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
1
UI/Web/.gitignore
vendored
|
|
@ -2,3 +2,4 @@ node_modules/
|
|||
test-results/
|
||||
playwright-report/
|
||||
i18n-cache-busting.json
|
||||
e2e-tests/environments/environment.local.ts
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
13043
UI/Web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
246
UI/Web/src/_card-item-common.scss
Normal file
246
UI/Web/src/_card-item-common.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
209
UI/Web/src/_series-detail-common.scss
Normal file
209
UI/Web/src/_series-detail-common.scss
Normal 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%);
|
||||
}
|
||||
}
|
||||
36
UI/Web/src/app/_directives/dbl-click.directive.ts
Normal file
36
UI/Web/src/app/_directives/dbl-click.directive.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
13
UI/Web/src/app/_directives/enter-blur.directive.ts
Normal file
13
UI/Web/src/app/_directives/enter-blur.directive.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
62
UI/Web/src/app/_helpers/browser.ts
Normal file
62
UI/Web/src/app/_helpers/browser.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
8
UI/Web/src/app/_models/collection/mal-stack.ts
Normal file
8
UI/Web/src/app/_models/collection/mal-stack.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface MalStack {
|
||||
title: string;
|
||||
stackId: number;
|
||||
url: string;
|
||||
author?: string;
|
||||
seriesCount: number;
|
||||
restackCount: number;
|
||||
}
|
||||
50
UI/Web/src/app/_models/common/i-has-cast.ts
Normal file
50
UI/Web/src/app/_models/common/i-has-cast.ts
Normal 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;
|
||||
}
|
||||
5
UI/Web/src/app/_models/common/i-has-cover.ts
Normal file
5
UI/Web/src/app/_models/common/i-has-cover.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export interface IHasCover {
|
||||
coverImage?: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
}
|
||||
4
UI/Web/src/app/_models/common/i-has-progress.ts
Normal file
4
UI/Web/src/app/_models/common/i-has-progress.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface IHasProgress {
|
||||
pages: number;
|
||||
pagesRead: number;
|
||||
}
|
||||
8
UI/Web/src/app/_models/common/i-has-reading-time.ts
Normal file
8
UI/Web/src/app/_models/common/i-has-reading-time.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface IHasReadingTime {
|
||||
minHoursToRead: number;
|
||||
maxHoursToRead: number;
|
||||
avgHoursToRead: number;
|
||||
pages: number;
|
||||
wordCount: number;
|
||||
|
||||
}
|
||||
1
UI/Web/src/app/_models/default-modal-options.ts
Normal file
1
UI/Web/src/app/_models/default-modal-options.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const DefaultModalOptions = {scrollable: true, size: 'xl', fullscreen: 'xl'};
|
||||
7
UI/Web/src/app/_models/email-history.ts
Normal file
7
UI/Web/src/app/_models/email-history.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export interface EmailHistory {
|
||||
sent: boolean;
|
||||
sendDate: string;
|
||||
emailTemplate: string;
|
||||
errorMessage: string;
|
||||
toUserName: string;
|
||||
}
|
||||
4
UI/Web/src/app/_models/events/chapter-removed-event.ts
Normal file
4
UI/Web/src/app/_models/events/chapter-removed-event.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface ChapterRemovedEvent {
|
||||
chapterId: number;
|
||||
seriesId: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export interface SiteThemeUpdatedEvent {
|
||||
themeName: string;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
4
UI/Web/src/app/_models/events/volume-removed-event.ts
Normal file
4
UI/Web/src/app/_models/events/volume-removed-event.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface VolumeRemovedEvent {
|
||||
volumeId: number;
|
||||
seriesId: number;
|
||||
}
|
||||
9
UI/Web/src/app/_models/kavitaplus/license-info.ts
Normal file
9
UI/Web/src/app/_models/kavitaplus/license-info.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export interface LicenseInfo {
|
||||
expirationDate: string;
|
||||
isActive: boolean;
|
||||
isCancelled: boolean;
|
||||
isValidVersion: boolean;
|
||||
registeredEmail: string;
|
||||
totalMonthsSubbed: number;
|
||||
hasLicense: boolean;
|
||||
}
|
||||
6
UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts
Normal file
6
UI/Web/src/app/_models/kavitaplus/manage-match-filter.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import {MatchStateOption} from "./match-state-option";
|
||||
|
||||
export interface ManageMatchFilter {
|
||||
matchStateOption: MatchStateOption;
|
||||
searchTerm: string;
|
||||
}
|
||||
7
UI/Web/src/app/_models/kavitaplus/manage-match-series.ts
Normal file
7
UI/Web/src/app/_models/kavitaplus/manage-match-series.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import {Series} from "../series";
|
||||
|
||||
export interface ManageMatchSeries {
|
||||
series: Series;
|
||||
isMatched: boolean;
|
||||
validUntilUtc: string;
|
||||
}
|
||||
11
UI/Web/src/app/_models/kavitaplus/match-state-option.ts
Normal file
11
UI/Web/src/app/_models/kavitaplus/match-state-option.ts
Normal 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
|
||||
];
|
||||
8
UI/Web/src/app/_models/kavitaplus/user-token-info.ts
Normal file
8
UI/Web/src/app/_models/kavitaplus/user-token-info.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export interface UserTokenInfo {
|
||||
userId: number;
|
||||
username: string;
|
||||
isAniListTokenSet: boolean;
|
||||
aniListValidUntilUtc: string;
|
||||
isAniListTokenValid: boolean;
|
||||
isMalTokenSet: boolean;
|
||||
}
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -43,4 +43,5 @@ export enum FilterComparison {
|
|||
/// Is Date not between now and X seconds ago
|
||||
/// </summary>
|
||||
IsNotInLast = 15,
|
||||
IsEmpty = 16
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
7
UI/Web/src/app/_models/metadata/v2/query-context.ts
Normal file
7
UI/Web/src/app/_models/metadata/v2/query-context.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export enum QueryContext
|
||||
{
|
||||
None = 1,
|
||||
Search = 2,
|
||||
Recommended = 3,
|
||||
Dashboard = 4,
|
||||
}
|
||||
6
UI/Web/src/app/_models/person/browse-person.ts
Normal file
6
UI/Web/src/app/_models/person/browse-person.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import {Person} from "../metadata/person";
|
||||
|
||||
export interface BrowsePerson extends Person {
|
||||
seriesCount: number;
|
||||
issueCount: number;
|
||||
}
|
||||
6
UI/Web/src/app/_models/preferences/pdf-layout-mode.ts
Normal file
6
UI/Web/src/app/_models/preferences/pdf-layout-mode.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export enum PdfLayoutMode {
|
||||
Multiple = 0,
|
||||
Single = 1,
|
||||
Book = 2,
|
||||
InfiniteScroll = 3
|
||||
}
|
||||
6
UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts
Normal file
6
UI/Web/src/app/_models/preferences/pdf-scroll-mode.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export enum PdfScrollMode {
|
||||
Vertical = 0,
|
||||
Horizontal = 1,
|
||||
Wrapped = 2,
|
||||
Page = 3
|
||||
}
|
||||
5
UI/Web/src/app/_models/preferences/pdf-spread-mode.ts
Normal file
5
UI/Web/src/app/_models/preferences/pdf-spread-mode.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export enum PdfSpreadMode {
|
||||
None = 0,
|
||||
Odd = 1,
|
||||
Even = 2
|
||||
}
|
||||
4
UI/Web/src/app/_models/preferences/pdf-theme.ts
Normal file
4
UI/Web/src/app/_models/preferences/pdf-theme.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export enum PdfTheme{
|
||||
Dark = 0,
|
||||
Light = 1
|
||||
}
|
||||
|
|
@ -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}];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
|
|||
11
UI/Web/src/app/_models/readers/full-progress.ts
Normal file
11
UI/Web/src/app/_models/readers/full-progress.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ export enum ScrobbleEventSortField {
|
|||
LastModified = 2,
|
||||
Type= 3,
|
||||
Series = 4,
|
||||
IsProcessed = 5
|
||||
IsProcessed = 5,
|
||||
ScrobbleEvent = 6
|
||||
}
|
||||
|
||||
export interface ScrobbleEventFilter {
|
||||
|
|
|
|||
|
|
@ -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> = [];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import {ExternalSeriesDetail} from "./external-series-detail";
|
||||
|
||||
export interface ExternalSeriesMatch {
|
||||
series: ExternalSeriesDetail;
|
||||
matchRating: number;
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
export interface HourEstimateRange{
|
||||
export interface HourEstimateRange {
|
||||
minHours: number;
|
||||
maxHours: number;
|
||||
avgHours: number;
|
||||
//hasProgress: boolean;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,4 +15,5 @@ export interface RelatedSeries {
|
|||
doujinshis: Array<Series>;
|
||||
parent: Array<Series>;
|
||||
editions: Array<Series>;
|
||||
annuals: Array<Series>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,4 +7,5 @@ export enum SideNavStreamType {
|
|||
ExternalSource = 6,
|
||||
AllSeries = 7,
|
||||
WantToRead = 8,
|
||||
BrowseAuthors = 9
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
9
UI/Web/src/app/_models/standalone-chapter.ts
Normal file
9
UI/Web/src/app/_models/standalone-chapter.ts
Normal 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;
|
||||
}
|
||||
4
UI/Web/src/app/_models/theme/colorscape.ts
Normal file
4
UI/Web/src/app/_models/theme/colorscape.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export interface ColorScape {
|
||||
primary?: string;
|
||||
secondary?: string;
|
||||
}
|
||||
10
UI/Web/src/app/_models/theme/downloadable-site-theme.ts
Normal file
10
UI/Web/src/app/_models/theme/downloadable-site-theme.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
24
UI/Web/src/app/_models/wiki.ts
Normal file
24
UI/Web/src/app/_models/wiki.ts
Normal 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'
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
21
UI/Web/src/app/_pipes/book-page-layout-mode.pipe.ts
Normal file
21
UI/Web/src/app/_pipes/book-page-layout-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
20
UI/Web/src/app/_pipes/confirm-translate.pipe.ts
Normal file
20
UI/Web/src/app/_pipes/confirm-translate.pipe.ts
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
25
UI/Web/src/app/_pipes/cover-image-size.pipe.ts
Normal file
25
UI/Web/src/app/_pipes/cover-image-size.pipe.ts
Normal 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');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
21
UI/Web/src/app/_pipes/encode-format.pipe.ts
Normal file
21
UI/Web/src/app/_pipes/encode-format.pipe.ts
Normal 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';
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
22
UI/Web/src/app/_pipes/layout-mode.pipe.ts
Normal file
22
UI/Web/src/app/_pipes/layout-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
16
UI/Web/src/app/_pipes/library-name.pipe.ts
Normal file
16
UI/Web/src/app/_pipes/library-name.pipe.ts
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
17
UI/Web/src/app/_pipes/log-level.pipe.ts
Normal file
17
UI/Web/src/app/_pipes/log-level.pipe.ts
Normal 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
27
UI/Web/src/app/_pipes/match-state.pipe.ts
Normal file
27
UI/Web/src/app/_pipes/match-state.pipe.ts
Normal 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');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
45
UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts
Normal file
45
UI/Web/src/app/_pipes/metadata-setting-filed.pipe.ts
Normal 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');
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
18
UI/Web/src/app/_pipes/page-layout-mode.pipe.ts
Normal file
18
UI/Web/src/app/_pipes/page-layout-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
22
UI/Web/src/app/_pipes/page-split-option.pipe.ts
Normal file
22
UI/Web/src/app/_pipes/page-split-option.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
UI/Web/src/app/_pipes/pdf-scroll-mode.pipe.ts
Normal file
21
UI/Web/src/app/_pipes/pdf-scroll-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
21
UI/Web/src/app/_pipes/pdf-spread-mode.pipe.ts
Normal file
21
UI/Web/src/app/_pipes/pdf-spread-mode.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
20
UI/Web/src/app/_pipes/pdf-theme.pipe.ts
Normal file
20
UI/Web/src/app/_pipes/pdf-theme.pipe.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
|||
25
UI/Web/src/app/_pipes/plus-media-format.pipe.ts
Normal file
25
UI/Web/src/app/_pipes/plus-media-format.pipe.ts
Normal 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
Loading…
Add table
Add a link
Reference in a new issue