Round 3 of Bugfixing (#3318)

This commit is contained in:
Joe Milazzo 2024-10-28 18:13:48 -05:00 committed by GitHub
parent 727fbd353b
commit abdf15b895
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 482 additions and 489 deletions

285
UI/Web/package-lock.json generated
View file

@ -464,6 +464,7 @@
"version": "18.2.9",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.9.tgz",
"integrity": "sha512-4iMoRvyMmq/fdI/4Gob9HKjL/jvTlCjbS4kouAYHuGO9w9dmUhi1pY1z+mALtCEl9/Q8CzU2W8e5cU2xtV4nVg==",
"dev": true,
"dependencies": {
"@babel/core": "7.25.2",
"@jridgewell/sourcemap-codec": "^1.4.14",
@ -491,6 +492,7 @@
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"dependencies": {
"readdirp": "^4.0.1"
},
@ -505,6 +507,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"engines": {
"node": ">= 14.16.0"
},
@ -1912,18 +1915,6 @@
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
@ -3730,14 +3721,6 @@
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/cacache": {
"version": "18.0.4",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz",
@ -4018,14 +4001,6 @@
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"dev": true
},
"node_modules/commander": {
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -4035,21 +4010,8 @@
"node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
},
"node_modules/copy-anything": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz",
"integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"is-what": "^3.14.1"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"dev": true
},
"node_modules/cosmiconfig": {
"version": "8.3.6",
@ -4556,6 +4518,7 @@
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
"integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
"dev": true,
"optional": true,
"dependencies": {
"iconv-lite": "^0.6.2"
@ -4565,6 +4528,7 @@
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"optional": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
@ -4612,20 +4576,6 @@
"integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
"dev": true
},
"node_modules/errno": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
"integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"prr": "~1.0.1"
},
"bin": {
"errno": "cli.js"
}
},
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@ -5491,20 +5441,6 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/image-size": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
"integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/immutable": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
@ -5700,14 +5636,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-what": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz",
"integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@ -5958,71 +5886,6 @@
"json-buffer": "3.0.1"
}
},
"node_modules/less": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz",
"integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"copy-anything": "^2.0.1",
"parse-node-version": "^1.0.1",
"tslib": "^2.3.0"
},
"bin": {
"lessc": "bin/lessc"
},
"engines": {
"node": ">=6"
},
"optionalDependencies": {
"errno": "^0.1.1",
"graceful-fs": "^4.1.2",
"image-size": "~0.5.0",
"make-dir": "^2.1.0",
"mime": "^1.4.1",
"needle": "^3.1.0",
"source-map": "~0.6.0"
}
},
"node_modules/less/node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
"integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"pify": "^4.0.1",
"semver": "^5.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/less/node_modules/semver": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"semver": "bin/semver"
}
},
"node_modules/less/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@ -6479,20 +6342,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
"dev": true,
"optional": true,
"peer": true,
"bin": {
"mime": "cli.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
@ -6775,38 +6624,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/needle": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"iconv-lite": "^0.6.3",
"sax": "^1.2.4"
},
"bin": {
"needle": "bin/needle"
},
"engines": {
"node": ">= 4.4.x"
}
},
"node_modules/needle/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@ -7378,17 +7195,6 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
},
"node_modules/parse-node-version": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
"integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">= 0.10"
}
},
"node_modules/parse5": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz",
@ -7507,17 +7313,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pify": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
"integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/piscina": {
"version": "4.6.1",
"resolved": "https://registry.npmjs.org/piscina/-/piscina-4.6.1.tgz",
@ -7598,14 +7393,6 @@
"node": ">=10"
}
},
"node_modules/prr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/psl": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz",
@ -7683,7 +7470,8 @@
"node_modules/reflect-metadata": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz",
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="
"integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==",
"dev": true
},
"node_modules/replace-in-file": {
"version": "7.1.0",
@ -7954,7 +7742,7 @@
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"devOptional": true
"dev": true
},
"node_modules/sass": {
"version": "1.77.6",
@ -7973,14 +7761,6 @@
"node": ">=14.0.0"
}
},
"node_modules/sax": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz",
"integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==",
"dev": true,
"optional": true,
"peer": true
},
"node_modules/screenfull": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/screenfull/-/screenfull-6.0.2.tgz",
@ -7996,6 +7776,7 @@
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"bin": {
"semver": "bin/semver.js"
},
@ -8165,29 +7946,6 @@
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/source-map-support/node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"optional": true,
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/spdx-correct": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
@ -8418,26 +8176,6 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/terser": {
"version": "5.31.6",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz",
"integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
"bin": {
"terser": "bin/terser"
},
"engines": {
"node": ">=10"
}
},
"node_modules/text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@ -8593,6 +8331,7 @@
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View file

@ -1,3 +1,5 @@
import {IHasCover} from "../common/i-has-cover";
export enum PersonRole {
Other = 1,
Artist = 2,
@ -16,7 +18,7 @@ export enum PersonRole {
Location = 15
}
export interface Person {
export interface Person extends IHasCover {
id: number;
name: string;
description: string;
@ -26,6 +28,6 @@ export interface Person {
aniListId?: number;
hardcoverId?: string;
asin?: string;
primaryColor?: string;
secondaryColor?: string;
primaryColor: string;
secondaryColor: string;
}

View file

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

View file

@ -18,6 +18,7 @@ import {FilterStatement} from "../_models/metadata/v2/filter-statement";
import {SeriesDetailPlus} from "../_models/series-detail/series-detail-plus";
import {LibraryType} from "../_models/library/library";
import {IHasCast} from "../_models/common/i-has-cast";
import {TextResonse} from "../_types/text-response";
@Injectable({
providedIn: 'root'
@ -77,6 +78,10 @@ export class MetadataService {
return this.httpClient.get<Array<Language>>(this.baseUrl + method);
}
getLanguageNameForCode(code: string) {
return this.httpClient.get<string>(`${this.baseUrl}metadata/language-title?code=${code}`, TextResonse);
}
/**
* All the potential language tags there can be

View file

@ -1,5 +1,43 @@
<ng-container *transloco="let t; read: 'details-tab'">
<div class="details pb-3">
@if (readingTime) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('read-time-title')}}</h4>
<div class="ms-3">
{{readingTime | readTime}}
</div>
</div>
}
@if (releaseYear) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('release-title')}}</h4>
<div class="ms-3">
{{releaseYear}}
</div>
</div>
}
@if (language) {
<div class="mb-3 ms-1">
<h4 class="header">{{t('language-title')}}</h4>
<div class="ms-3">
{{language | languageName | async}}
</div>
</div>
}
<div class="mb-3 ms-1">
<h4 class="header">{{t('format-title')}}</h4>
<div class="ms-3">
<app-series-format [format]="format"></app-series-format> {{format | mangaFormat }}
</div>
</div>
<div class="setting-section-break" aria-hidden="true"></div>
<div class="mb-3 ms-1">
<h4 class="header">{{t('genres-title')}}</h4>
<div class="ms-3">

View file

@ -3,18 +3,25 @@ import {CarouselReelComponent} from "../../carousel/_components/carousel-reel/ca
import {PersonBadgeComponent} from "../../shared/person-badge/person-badge.component";
import {TranslocoDirective} from "@jsverse/transloco";
import {IHasCast} from "../../_models/common/i-has-cast";
import {Person, PersonRole} from "../../_models/metadata/person";
import {Router} from "@angular/router";
import {PersonRole} from "../../_models/metadata/person";
import {FilterField} from "../../_models/metadata/v2/filter-field";
import {FilterComparison} from "../../_models/metadata/v2/filter-comparison";
import {FilterUtilitiesService} from "../../shared/_services/filter-utilities.service";
import {Genre} from "../../_models/metadata/genre";
import {Tag} from "../../_models/tag";
import {TagBadgeComponent, TagBadgeCursor} from "../../shared/tag-badge/tag-badge.component";
import {TagBadgeComponent} from "../../shared/tag-badge/tag-badge.component";
import {ImageComponent} from "../../shared/image/image.component";
import {SafeHtmlPipe} from "../../_pipes/safe-html.pipe";
import {ImageService} from "../../_services/image.service";
import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander.component";
import {IHasReadingTime} from "../../_models/common/i-has-reading-time";
import {ReadTimePipe} from "../../_pipes/read-time.pipe";
import {SentenceCasePipe} from "../../_pipes/sentence-case.pipe";
import {MangaFormat} from "../../_models/manga-format";
import {SeriesFormatComponent} from "../../shared/series-format/series-format.component";
import {MangaFormatPipe} from "../../_pipes/manga-format.pipe";
import {LanguageNamePipe} from "../../_pipes/language-name.pipe";
import {AsyncPipe} from "@angular/common";
@Component({
selector: 'app-details-tab',
@ -26,7 +33,13 @@ import {BadgeExpanderComponent} from "../../shared/badge-expander/badge-expander
TagBadgeComponent,
ImageComponent,
SafeHtmlPipe,
BadgeExpanderComponent
BadgeExpanderComponent,
ReadTimePipe,
SentenceCasePipe,
SeriesFormatComponent,
MangaFormatPipe,
LanguageNamePipe,
AsyncPipe
],
templateUrl: './details-tab.component.html',
styleUrl: './details-tab.component.scss',
@ -41,6 +54,10 @@ export class DetailsTabComponent {
protected readonly FilterField = FilterField;
@Input({required: true}) metadata!: IHasCast;
@Input() readingTime: IHasReadingTime | undefined;
@Input() language: string | undefined;
@Input() format: MangaFormat = MangaFormat.UNKNOWN;
@Input() releaseYear: number | undefined;
@Input() genres: Array<Genre> = [];
@Input() tags: Array<Tag> = [];
@Input() webLinks: Array<string> = [];
@ -50,4 +67,6 @@ export class DetailsTabComponent {
if (queryParamName === FilterField.None) return;
this.filterUtilityService.applyFilter(['all-series'], queryParamName, FilterComparison.Equal, `${filter}`).subscribe();
}
protected readonly MangaFormat = MangaFormat;
}

View file

@ -493,7 +493,7 @@ export class EditChapterModalComponent implements OnInit {
};
personSettings.addTransformFn = ((title: string) => {
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false };
return {id: 0, name: title, role: role, description: '', coverImage: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
});
return personSettings;

View file

@ -52,7 +52,7 @@
<div class="form-group mb-3">
<label for="discordId">{{t('activate-discordId-label')}}</label>
<i class="fa fa-circle-info ms-1" aria-hidden="true" [ngbTooltip]="t('activate-discordId-tooltip')"></i>
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">Help</a>
<a class="ms-1" [href]="WikiLink.KavitaPlusDiscordId" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
<input id="discordId" type="text" class="form-control" formControlName="discordId" autocomplete="off" [class.is-invalid]="formGroup.get('discordId')?.invalid && formGroup.get('discordId')?.touched"/>
@if (formGroup.dirty || formGroup.touched) {
<div id="inviteForm-validations" class="invalid-feedback">

View file

@ -14,6 +14,6 @@
</app-side-nav-companion-bar>
<app-manage-smart-filters></app-manage-smart-filters>
<app-manage-smart-filters [target]="'_self'"></app-manage-smart-filters>
</div>
</ng-container>

View file

@ -198,7 +198,7 @@
<ng-container [ngTemplateOutlet]="lock" [ngTemplateOutletContext]="{ item: metadata, field: 'publicationStatusLocked' }"></ng-container>
<select class="form-select" id="publication-status" formControlName="publicationStatus">
@for (opt of publicationStatuses; track opt.value) {
<option [value]="opt.value">{{opt.title | titlecase}}</option>
<option [value]="opt.value">{{opt.value | publicationStatus}}</option>
}
</select>
</div>

View file

@ -515,7 +515,7 @@ export class EditSeriesModalComponent implements OnInit {
};
personSettings.addTransformFn = ((title: string) => {
return {id: 0, name: title, description: '', coverImageLocked: false };
return {id: 0, name: title, description: '', coverImageLocked: false, primaryColor: '', secondaryColor: '' };
});
return personSettings;
@ -551,6 +551,7 @@ export class EditSeriesModalComponent implements OnInit {
// We only need to call updateSeries if we changed name, sort name, or localized name or reset a cover image
const nameFieldsDirty = this.editSeriesForm.get('name')?.dirty || this.editSeriesForm.get('sortName')?.dirty || this.editSeriesForm.get('localizedName')?.dirty;
const nameFieldLockChanged = this.series.nameLocked !== this.initSeries.nameLocked || this.series.sortNameLocked !== this.initSeries.sortNameLocked || this.series.localizedNameLocked !== this.initSeries.localizedNameLocked;
if (nameFieldsDirty || nameFieldLockChanged || this.coverImageReset) {
model.nameLocked = this.series.nameLocked;
model.sortNameLocked = this.series.sortNameLocked;

View file

@ -46,12 +46,12 @@
<div class="card-overlay"></div>
<div class="chapter overlay-information">
<div class="overlay-information--centered">
<span class="card-title library mx-auto" style="width: auto;" (click)="read($event)">
<!-- Card Image -->
<div>
<i class="fa-solid fa-book" aria-hidden="true"></i>
</div>
</span>
<span class="card-title library mx-auto" style="width: auto;" (click)="read($event)">
<!-- Card Image -->
<div>
<i class="fa-solid fa-book" aria-hidden="true"></i>
</div>
</span>
</div>
</div>
</div>
@ -85,10 +85,10 @@
</a>
</span>
<span class="card-actions">
@if (actions && actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
}
</span>
@if (actions && actions.length > 0) {
<app-card-actionables (actionHandler)="performAction($event)" [actions]="actions" [labelBy]="chapter.titleName"></app-card-actionables>
}
</span>
</div>
</div>

View file

@ -1,32 +1,31 @@
<ng-container *transloco="let t; read: 'cover-image-chooser'">
<div class="container-fluid" style="padding-left: 0px; padding-right: 0px">
<div class="container-fluid" style="padding-left: 0; padding-right: 0">
<form [formGroup]="form">
<ngx-file-drop (onFileDrop)="dropped($event)"
(onFileOver)="fileOver($event)" (onFileLeave)="fileLeave($event)" [accept]="acceptableExtensions" [directory]="false"
dropZoneClassName="file-upload" contentClassName="file-upload-zone">
<ng-template ngx-file-drop-content-tmp let-openFileSelector="openFileSelector">
<div class="row g-0 mt-3 pb-3" *ngIf="mode === 'all'">
<div class="mx-auto">
<div class="row g-0 mb-3">
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
</div>
@if (mode === 'all') {
<div class="row g-0 mt-3 pb-3">
<div class="mx-auto">
<div class="row g-0 mb-3">
<i class="fa fa-file-upload mx-auto" style="font-size: 24px; width: 20px;" aria-hidden="true"></i>
</div>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
<a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="ps-1 pe-1"></span>
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-evenly">
<a class="pe-0" href="javascript:void(0)" (click)="changeMode('url')">
<span class="phone-hidden">{{t('enter-an-url-pre-title', {url: ''})}}</span>{{t('url')}}
</a>
<span class="ps-1 pe-1"></span>
<span class="pe-0" href="javascript:void(0)">{{t('drag-n-drop')}}</span>
<span class="ps-1 pe-1"></span>
<a class="pe-0" href="javascript:void(0)" (click)="openFileSelector()">{{t('upload')}}<span class="phone-hidden"> {{t('upload-continued')}}</span></a>
</div>
</div>
</div>
</div>
</div>
<ng-container *ngIf="mode === 'url'">
} @else if (mode === 'url') {
<div class="row g-0 mt-3 pb-3 ms-md-2 me-md-2">
<div class="input-group col-auto me-md-2" style="width: 83%">
<label class="input-group-text" for="load-image">{{t('url-label')}}</label>
@ -42,9 +41,7 @@
<span class="phone-hidden">{{t('back')}}</span>
</button>
</div>
</ng-container>
}
</ng-template>
</ngx-file-drop>
@ -54,28 +51,32 @@
</form>
<div class="row g-0 chooser" style="padding-top: 10px">
<div class="clickable col-auto"
*ngIf="showReset" tabindex="0" (click)="reset()"
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="232.91px" width="160px" [imageUrl]="imageService.resetCoverImage"></app-image>
<ng-container *ngIf="showApplyButton">
<br>
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
</ng-container>
@if (showReset) {
<div class="clickable col-auto" tabindex="0" (click)="reset()"
[ngClass]="{'selected': !showApplyButton && selectedIndex === -1}">
<app-image class="card-img-top" [title]="t('reset-cover-tooltip')" height="232.91px" width="160px" [imageUrl]="imageService.resetCoverImage"></app-image>
@if (showApplyButton) {
<br>
<button style="width: 100%;" class="btn btn-secondary" (click)="resetImage()">{{t('reset')}}</button>
}
</div>
}
@for (url of imageUrls; track url; let idx = $index) {
<div class="clickable col-auto" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
<app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
@if (showApplyButton) {
<br>
<button class="btn btn-primary" style="width: 100%;"
(click)="applyImage(idx)">
{{appliedIndex === idx ? t('applied') : t('apply')}}
</button>
}
</div>
}
</div>
<div class="clickable col-auto"
*ngFor="let url of imageUrls; let idx = index;" tabindex="0" [attr.aria-label]="t('image-num', {num: idx + 1})" (click)="selectImage(idx)"
[ngClass]="{'selected': !showApplyButton && selectedIndex === idx}">
<app-image class="card-img-top" height="232.91px" width="160px" [imageUrl]="url" [processEvents]="idx > 0"></app-image>
<ng-container *ngIf="showApplyButton">
<br>
<button class="btn btn-primary" style="width: 100%;"
(click)="applyImage(idx)">
{{appliedIndex === idx ? t('applied') : t('apply')}}
</button>
</ng-container>
</div>
</div>
</div>

View file

@ -17,7 +17,7 @@ import { ToastrService } from 'ngx-toastr';
import { ImageService } from 'src/app/_services/image.service';
import { KEY_CODES } from 'src/app/shared/_services/utility.service';
import { UploadService } from 'src/app/_services/upload.service';
import {CommonModule, DOCUMENT} from '@angular/common';
import {DOCUMENT, NgClass} from '@angular/common';
import {ImageComponent} from "../../shared/image/image.component";
import {translate, TranslocoModule} from "@jsverse/transloco";
@ -27,9 +27,9 @@ import {translate, TranslocoModule} from "@jsverse/transloco";
imports: [
ReactiveFormsModule,
NgxFileDropModule,
CommonModule,
ImageComponent,
TranslocoModule
TranslocoModule,
NgClass
],
templateUrl: './cover-image-chooser.component.html',
styleUrls: ['./cover-image-chooser.component.scss'],

View file

@ -1,87 +1,94 @@
<ng-container *transloco="let t; read: 'entity-title'">
@switch (libraryType) {
@case (LibraryType.Comic) {
@if (titleName !== '' && prioritizeTitleName) {
@if (isChapter && includeChapter) {
{{t('issue-num') + ' ' + number + ' - ' }}
}
{{renderText | defaultValue}}
<!-- @switch (libraryType) {-->
<!-- @case (LibraryType.Comic) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
{{titleName}}
} @else {
@if (includeVolume && volumeTitle !== '') {
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
}
{{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}
}
}
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
<!-- }-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}-->
<!-- }-->
<!-- }-->
@case (LibraryType.ComicVine) {
@if (titleName !== '' && prioritizeTitleName) {
@if (isChapter && includeChapter) {
{{t('issue-num') + ' ' + number + ' - ' }}
}
<!-- @case (LibraryType.ComicVine) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- {{t('issue-num') + ' ' + number + ' - ' }}-->
<!-- }-->
{{titleName}}
} @else {
@if (includeVolume && volumeTitle !== '') {
{{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}
}
{{number !== LooseLeafOrSpecial ? (isChapter ? t('issue-num') + number : volumeTitle) : t('special')}}
}
}
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter && includeVolume ? volumeTitle : '') : ''}}-->
<!-- }-->
<!-- @if (number !== LooseLeafOrSpecial) {-->
<!-- {{isChapter ? t('issue-num') + number : volumeTitle}}-->
<!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
@case (LibraryType.Manga) {
@if (titleName !== '' && prioritizeTitleName) {
@if (isChapter && includeChapter) {
@if (number === LooseLeafOrSpecial) {
{{t('chapter') + ' - ' }}
} @else {
{{t('chapter') + ' ' + number + ' - ' }}
}
<!-- @case (LibraryType.Manga) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- @if (isChapter && includeChapter) {-->
<!-- @if (number === LooseLeafOrSpecial) {-->
<!-- {{t('chapter') + ' - ' }}-->
<!-- } @else {-->
<!-- {{t('chapter') + ' ' + number + ' - ' }}-->
<!-- }-->
}
{{titleName}}
} @else {
@if (includeVolume && volumeTitle !== '') {
@if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {
{{volumeTitle}}
}
}
<!-- }-->
<!-- {{titleName}}-->
<!-- } @else {-->
<!-- @if (includeVolume && volumeTitle !== '') {-->
<!-- @if (number !== LooseLeafOrSpecial && isChapter && includeVolume) {-->
<!-- {{volumeTitle}}-->
<!-- }-->
<!-- }-->
@if (number !== LooseLeafOrSpecial) {
@if (isChapter) {
{{t('chapter') + ' ' + number}}
} @else {
{{volumeTitle}}
}
} @else {
{{t('special')}}
}
}
}
<!-- @if (number !== LooseLeafOrSpecial) {-->
<!-- @if (isChapter) {-->
<!-- {{t('chapter') + ' ' + number}}-->
<!-- } @else {-->
<!-- {{volumeTitle}}-->
<!-- }-->
<!-- } @else if (fallbackToVolume && isChapter && volumeTitle) {-->
<!-- {{t('vol-num', {num: volumeTitle})}}-->
<!-- } @else {-->
<!-- {{t('special')}}-->
<!-- }-->
<!-- }-->
<!-- }-->
@case (LibraryType.Book) {
@if (titleName !== '' && prioritizeTitleName) {
{{titleName}}
} @else if (number === LooseLeafOrSpecial) {
{{null | defaultValue}}
} @else {
{{t('book-num', {num: volumeTitle})}}
}
}
<!-- @case (LibraryType.Book) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- {{titleName}}-->
<!-- } @else if (number === LooseLeafOrSpecial) {-->
<!-- {{null | defaultValue}}-->
<!-- } @else {-->
<!-- {{t('book-num', {num: volumeTitle})}}-->
<!-- }-->
<!-- }-->
@case (LibraryType.LightNovel) {
@if (titleName !== '' && prioritizeTitleName) {
{{titleName}}
} @else if (number === LooseLeafOrSpecial) {
{{null | defaultValue}}
} @else {
{{t('book-num', {num: (isChapter ? number : volumeTitle)})}}
}
}
<!-- @case (LibraryType.LightNovel) {-->
<!-- @if (titleName !== '' && prioritizeTitleName) {-->
<!-- {{titleName}}-->
<!-- } @else if (number === LooseLeafOrSpecial) {-->
<!-- {{null | defaultValue}}-->
<!-- } @else {-->
<!-- {{t('book-num', {num: (isChapter ? number : volumeTitle)})}}-->
<!-- }-->
<!-- }-->
@case (LibraryType.Images) {
{{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}
}
}
<!-- @case (LibraryType.Images) {-->
<!-- {{number !== LooseLeafOrSpecial ? (isChapter ? (t('chapter') + ' ') + number : volumeTitle) : t('special')}}-->
<!-- }-->
<!-- }-->
</ng-container>

View file

@ -1,9 +1,9 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnInit } from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input, OnInit} from '@angular/core';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { Chapter, LooseLeafOrDefaultNumber } from 'src/app/_models/chapter';
import { LibraryType } from 'src/app/_models/library/library';
import { Volume } from 'src/app/_models/volume';
import {TranslocoModule} from "@jsverse/transloco";
import {translate, TranslocoModule} from "@jsverse/transloco";
import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
/**
@ -22,6 +22,9 @@ import {DefaultValuePipe} from "../../_pipes/default-value.pipe";
})
export class EntityTitleComponent implements OnInit {
private readonly utilityService = inject(UtilityService);
private readonly cdRef = inject(ChangeDetectorRef);
protected readonly LooseLeafOrSpecial = LooseLeafOrDefaultNumber + "";
protected readonly LibraryType = LibraryType;
@ -42,16 +45,18 @@ export class EntityTitleComponent implements OnInit {
* When a titleName (aka a title) is available on the entity, show it over Volume X Chapter Y
*/
@Input() prioritizeTitleName: boolean = true;
/**
* When there is no meaningful title to display and the chapter is just a single volume, show the volume number
*/
@Input() fallbackToVolume: boolean = true;
isChapter = false;
titleName: string = '';
volumeTitle: string = '';
number: string = '';
renderText: string = '';
constructor(private utilityService: UtilityService, private readonly cdRef: ChangeDetectorRef) {}
ngOnInit(): void {
this.isChapter = this.utilityService.isChapter(this.entity);
@ -60,7 +65,6 @@ export class EntityTitleComponent implements OnInit {
this.volumeTitle = c.volumeTitle || '';
this.titleName = c.titleName || '';
this.number = c.range;
} else {
const v = this.utilityService.asVolume(this.entity);
this.volumeTitle = v.name || '';
@ -70,6 +74,125 @@ export class EntityTitleComponent implements OnInit {
}
this.number = v.name;
}
this.calculateRenderText();
this.cdRef.markForCheck();
}
private calculateRenderText() {
switch (this.libraryType) {
case LibraryType.Manga:
this.renderText = this.calculateMangaRenderText();
break;
case LibraryType.Comic:
this.renderText = this.calculateComicRenderText();
break;
case LibraryType.Book:
this.renderText = this.calculateBookRenderText();
break;
case LibraryType.Images:
this.renderText = this.calculateImageRenderText();
break;
case LibraryType.LightNovel:
this.renderText = this.calculateLightNovelRenderText();
break;
case LibraryType.ComicVine:
this.renderText = this.calculateComicRenderText();
break;
}
this.cdRef.markForCheck();
}
private calculateBookRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
renderText = this.titleName;
} else if (this.number === this.LooseLeafOrSpecial) {
renderText = '';
} else {
renderText = translate('entity-title.book-num', {num: this.volumeTitle});
}
return renderText;
}
private calculateLightNovelRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
renderText = this.titleName;
} else if (this.number === this.LooseLeafOrSpecial) {
renderText = '';
} else {
const bookNum = this.isChapter ? this.number : this.volumeTitle;
renderText = translate('entity-title.book-num', {num: bookNum});
}
return renderText;
}
private calculateMangaRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
if (this.isChapter && this.includeChapter) {
if (this.number === this.LooseLeafOrSpecial) {
renderText = translate('entity-title.chapter') + ' - ';
} else {
renderText = translate('entity-title.chapter') + ' ' + this.number + ' - ';
}
}
renderText += this.titleName;
} else {
if (this.includeVolume && this.volumeTitle !== '') {
if (this.number !== this.LooseLeafOrSpecial && this.isChapter && this.includeVolume) {
renderText = this.volumeTitle;
}
}
if (this.number !== this.LooseLeafOrSpecial) {
if (this.isChapter) {
renderText = translate('entity-title.chapter') + ' ' + this.number;
} else {
renderText = this.volumeTitle;
}
} else if (this.fallbackToVolume && this.isChapter && this.volumeTitle) {
renderText = translate('entity-title.vol-num', {num: this.volumeTitle});
} else if (this.fallbackToVolume && this.isChapter) { // this.volumeTitle === '' (this is a single volume on volume detail page)
renderText = translate('entity-title.single-volume');
} else {
renderText = translate('entity-title.special');
}
}
return renderText;
}
private calculateImageRenderText() {
let renderText = '';
if (this.number !== this.LooseLeafOrSpecial) {
if (this.isChapter) {
renderText = translate('entity-title.chapter') + ' ' + this.number;
} else {
renderText = this.volumeTitle;
}
} else {
renderText = translate('entity-title.special');
}
return renderText;
}
private calculateComicRenderText() {
let renderText = '';
if (this.titleName !== '' && this.prioritizeTitleName) {
if (this.isChapter && this.includeChapter) {
renderText = translate('entity-title.issue-num') + ' ' + this.number + ' - ';
}
renderText += this.titleName;
}
return renderText;
}
}

View file

@ -163,7 +163,13 @@
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="chapter" [genres]="chapter.genres" [tags]="chapter.tags" [webLinks]="weblinks"></app-details-tab>
<app-details-tab [metadata]="chapter"
[genres]="chapter.genres"
[tags]="chapter.tags"
[webLinks]="weblinks"
[readingTime]="chapter"
[language]="chapter.language"
[format]="series.format"></app-details-tab>
}
</ng-template>
</li>

View file

@ -59,7 +59,9 @@
<div class="row mt-2">
<app-carousel-reel [items]="(chaptersByRole[role] | async)!" [title]="t('individual-role-title', {role: (role | personRole)})" (sectionClick)="loadFilterByRole(role)">
<ng-template #carouselItem let-item>
<app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId"></app-chapter-card>
<app-chapter-card [chapter]="item" [libraryId]="item.libraryId" [libraryType]="item.libraryType" [seriesId]="item.seriesId">
</app-chapter-card>
</ng-template>
</app-carousel-reel>
</div>

View file

@ -165,7 +165,7 @@
<!-- Spacer -->
<div class="col" aria-hidden="true"></div>
<div class="col-auto ms-1">
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">Help</a>
<a class="btn btn-icon" [href]="WikiLink.ReadingListCBL" target="_blank" rel="noopener noreferrer">{{t('help-label')}}</a>
</div>
<div class="col-auto ms-1">
<button type="button" class="btn btn-primary" (click)="prevStep()" [disabled]="!canMoveToPrevStep()">{{t('prev')}}</button>

View file

@ -187,7 +187,7 @@
<virtual-scroller #scroll [items]="storylineItems" [bufferAmount]="1" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for(item of scroll.viewPortItems; let idx = $index; track item) {
@if (item.isChapter) {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item.chapter, scroll: scroll, idx: idx, chaptersLength: storyChapters.length}"></ng-container>
} @else {
@ -214,7 +214,7 @@
@defer (when activeTabId === TabID.Volumes; prefetch on idle) {
<virtual-scroller #scroll [items]="volumes" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + + '_volumes') {
<ng-container [ngTemplateOutlet]="nonChapterVolumeCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: volumes.length}"></ng-container>
}
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Volumes}"></ng-container>
@ -235,7 +235,7 @@
@defer (when activeTabId === TabID.Chapters; prefetch on idle) {
<virtual-scroller #scroll [items]="chapters" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for (item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_chapters') {
<ng-container [ngTemplateOutlet]="nonSpecialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, totalLength: chapters.length}"></ng-container>
}
<ng-container [ngTemplateOutlet]="estimatedNextCard" [ngTemplateOutletContext]="{tabId: TabID.Chapters}"></ng-container>
@ -256,7 +256,7 @@
@defer (when activeTabId === TabID.Specials; prefetch on idle) {
<virtual-scroller #scroll [items]="specials" [parentScroll]="scrollingBlock" [childHeight]="1">
<div class="card-container row g-0" #container>
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead) {
@for(item of scroll.viewPortItems; let idx = $index; track item.id + '_' + item.pagesRead + '_specials') {
<ng-container [ngTemplateOutlet]="specialChapterCard" [ngTemplateOutletContext]="{$implicit: item, scroll: scroll, idx: idx, chaptersLength: chapters.length}"></ng-container>
}
</div>
@ -338,7 +338,15 @@
<a ngbNavLink>{{t(TabID.Details)}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="seriesMetadata" [genres]="seriesMetadata.genres" [tags]="seriesMetadata.tags" [webLinks]="WebLinks"></app-details-tab>
<app-details-tab [metadata]="seriesMetadata"
[genres]="seriesMetadata.genres"
[tags]="seriesMetadata.tags"
[webLinks]="WebLinks"
[readingTime]="series"
[releaseYear]="seriesMetadata.releaseYear"
[language]="seriesMetadata.language"
[format]="series.format">
</app-details-tab>
}
</ng-template>
</li>

View file

@ -19,7 +19,7 @@
<i class="fa-solid fa-triangle-exclamation red me-2" [ngbTooltip]="t('errored')"></i>
<span class="visually-hidden">{{t('errored')}}</span>
}
<a [href]="baseUrl + 'all-series?' + f.filter" target="_blank">{{f.name}}</a>
<a [href]="baseUrl + 'all-series?' + f.filter" [target]="target">{{f.name}}</a>
</span>
<button class="btn btn-danger float-end" (click)="deleteFilter(f)">
<i class="fa-solid fa-trash" aria-hidden="true"></i>

View file

@ -1,4 +1,4 @@
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject} from '@angular/core';
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, inject, Input} from '@angular/core';
import {FilterService} from "../../../_services/filter.service";
import {SmartFilter} from "../../../_models/metadata/v2/smart-filter";
import {TranslocoDirective} from "@jsverse/transloco";
@ -24,6 +24,8 @@ export class ManageSmartFiltersComponent {
private readonly actionService = inject(ActionService);
protected readonly baseUrl = inject(APP_BASE_HREF);
@Input() target: '_self' | '_blank' = '_blank';
filters: Array<SmartFilter> = [];
listForm: FormGroup = new FormGroup({
'filterQuery': new FormControl('', [])

View file

@ -196,7 +196,12 @@
<a ngbNavLink>{{t('details-tab')}}</a>
<ng-template ngbNavContent>
@defer (when activeTabId === TabID.Details; prefetch on idle) {
<app-details-tab [metadata]="volumeCast" [genres]="genres" [tags]="tags"></app-details-tab>
<app-details-tab [metadata]="volumeCast"
[genres]="genres"
[tags]="tags"
[readingTime]="volume"
[language]="volume.chapters[0].language"
[format]="series.format"></app-details-tab>
}
</ng-template>
</li>

View file

@ -668,6 +668,7 @@
"license-valid": "License is Valid",
"license-not-valid": "License Not Valid",
"loading": "{{common.loading}}",
"help-label": "{{common.help}}",
"activate-description": "Enter the License Key and Email used to register with Stripe",
"activate-license-label": "License Key",
@ -1071,7 +1072,12 @@
"imprints-title": "{{metadata-fields.imprints-title}}",
"genres-title": "{{metadata-fields.genres-title}}",
"tags-title": "{{metadata-fields.tags-title}}",
"weblinks-title": "{{tabs.weblink-tab}}"
"weblinks-title": "{{tabs.weblink-tab}}",
"read-time-title": "{{edit-chapter-modal.reading-time-label}}",
"language-title": "{{edit-chapter-modal.language-label}}",
"release-title": "{{sort-field-pipe.release-year}}",
"format-title": "{{metadata-filter.format-label}}",
"length-title": "{{edit-chapter-modal.words-label}}"
},
"related-tab": {
@ -1126,7 +1132,9 @@
"special": "Special",
"issue-num": "{{common.issue-hash-num}}",
"chapter": "{{common.chapter-num}}",
"book-num": "{{common.book-num-shorthand}}"
"book-num": "{{common.book-num-shorthand}}",
"vol-num": "{{user-scrobble-history.volume-num}}",
"single-volume": "Single Volume"
},
"external-series-card": {
@ -1691,6 +1699,7 @@
"import-cbl-modal": {
"close": "{{common.close}}",
"title": "CBL Import",
"help-label": "{{common.help}}",
"import-description": "To get started, import a .cbl file. Kavita will perform multiple checks before importing. Some steps will block moving forward due to issues with the file.",
"validate-description": "All files have been validated to see if there are any operations to do on the list. Any lists that have failed will not move to the next step. Fix the CBL files and retry.",
"validate-warning": "There are issues with the CBL that will prevent an import. Correct these issues then try again.",