Local Metadata Integration Part 1 (#817)

* Started with some basic plumbing with comic info parsing updating Series/Volume.

* We can now get chapter title from comicInfo.xml

* Hooked in the ability to store people into the chapter metadata.

* Removed no longer used imports, fixed up some foreign key constraints on deleting series with person linked.

* Refactored Summary out of the UI for Series into SeriesMetadata. Updated application to .net 6. There is a bug in metadata code for updating.

* Removed the parallel.ForEach with a normal foreach which lets us use async. For I/O heavy code, shouldn't change much.

* Refactored scan code to only check extensions with comic info, fixed a bug on scan events not using correct method name, removed summary field (still buggy)

* Fixed a bug where on cancelling a metadata request in modal, underlying button would get stuck in a disabled state.

* Changed how metadata selects the first volume to read summary info from. It will now select the first non-special volume rather than Volume 1.

* More debugging and found more bugs to fix

* Redid all the migrations as one single one. Fixed a bug with GetChapterInfo returning null when ChapterMetadata didn't exist for that Chapter.

Fixed an issue with mapper failing on GetChapterMetadata. Started work on adding people and a design for people.

* Fixed a bug where checking if file modified now takes into account if file has been processed at least once. Introduced a bug in saving people to series.

* Just made code compilable again

* Fixed up code. Now people for series and chapters add correctly without any db issues.

* Things are working, but I'm not happy with how the management of Person is. I need to take into account that 1 person needs to map to an image and role is arbitrary.

* Started adding UI code to showcase chapter metadata

* Updated workflow to be .NET 6

* WIP of updating card detail to show the information more clearly and without so many if statements

* Removed ChatperMetadata and store on the Chapter itself. Much easier to use and less joins.

* Implemented Genre on SeriesMetadata level

* Genres and People are now removed from Series level if they are no longer on comicInfo

* PeopleHelper is done with unit tests. Everything is working.

* Unit tests in place for Genre Helper

* Starting on CacheHelper

* Finished tests for ShouldUpdateCoverImage. Fixed and added tests in ArchiveService/ScannerService.

* CacheHelper is fully tested

* Some DI cleanup

* Scanner Service now calls GetComicInfo for books. Added ability to update Series Sort name from metadata files (mainly epub as comicinfo doesn't have a field)

* Forgot to move a line of code

* SortName now populates from metadata (epub only, ComicInfo has no tags)

* Cards now show the chapter title name if it's set on hover, else will default back to title.

* Fixed a major issue with how MangaFiles were being updated with LastModified, which messed up our logic for avoiding refreshes.

* Woohoo, more tests and some refactors to be able to test more services wtih mock filesystem. Fixed an issue where SortName was getting set as first chapter, but the Series was in a group.

* Refactored the MangaFile creation code into the DbFactory where we also setup the first LastModified update.

* Has file changed bug is now finally fixed

* Remove dead genres, refactor genre to use title instead of name.

* Refactored out a directory from ShouldUpdateCoverImage() to keep the code clean

* Unit tests for ComicInfo on BookService.

* Refactored series detail into it's own component

* Series-detail now received refresh metadata events to refresh what's on screen

* Removed references to Artist on PersonRole as it has no metadata mapping

* Security audit

* Fixed a benchmark

* Updated JWT Token generator to use new methods in .NET 6

* Updated all the docker and build commands to use net6.0

* Commented out sonar scan since it's not setup for net6.0 yet.
This commit is contained in:
Joseph Milazzo 2021-12-02 11:02:34 -06:00 committed by GitHub
parent 10a6a3a544
commit e7619e6b0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
140 changed files with 9315 additions and 1545 deletions

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

@ -5326,9 +5326,9 @@
"dev": true
},
"decimal.js": {
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz",
"integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==",
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz",
"integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==",
"dev": true
},
"decode-uri-component": {
@ -5352,9 +5352,9 @@
}
},
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=",
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"deepmerge": {
@ -5956,18 +5956,24 @@
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"escodegen": {
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz",
"integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==",
"dev": true,
"requires": {
"esprima": "^4.0.1",
"estraverse": "^4.2.0",
"estraverse": "^5.2.0",
"esutils": "^2.0.2",
"optionator": "^0.8.1",
"source-map": "~0.6.1"
},
"dependencies": {
"estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@ -7856,9 +7862,9 @@
}
},
"is-potential-custom-element-name": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz",
"integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"dev": true
},
"is-regex": {
@ -9593,66 +9599,91 @@
"dev": true
},
"jsdom": {
"version": "16.4.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.4.0.tgz",
"integrity": "sha512-lYMm3wYdgPhrl7pDcRmvzPhhrGVBeVhPIqeHjzeiHN3DFmD1RBpbExbi8vU7BJdH8VAZYovR8DMt0PNNDM7k8w==",
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
"integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==",
"dev": true,
"requires": {
"abab": "^2.0.3",
"acorn": "^7.1.1",
"abab": "^2.0.5",
"acorn": "^8.2.4",
"acorn-globals": "^6.0.0",
"cssom": "^0.4.4",
"cssstyle": "^2.2.0",
"cssstyle": "^2.3.0",
"data-urls": "^2.0.0",
"decimal.js": "^10.2.0",
"decimal.js": "^10.2.1",
"domexception": "^2.0.1",
"escodegen": "^1.14.1",
"escodegen": "^2.0.0",
"form-data": "^3.0.0",
"html-encoding-sniffer": "^2.0.1",
"is-potential-custom-element-name": "^1.0.0",
"http-proxy-agent": "^4.0.1",
"https-proxy-agent": "^5.0.0",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.0",
"parse5": "5.1.1",
"request": "^2.88.2",
"request-promise-native": "^1.0.8",
"saxes": "^5.0.0",
"parse5": "6.0.1",
"saxes": "^5.0.1",
"symbol-tree": "^3.2.4",
"tough-cookie": "^3.0.1",
"tough-cookie": "^4.0.0",
"w3c-hr-time": "^1.0.2",
"w3c-xmlserializer": "^2.0.0",
"webidl-conversions": "^6.1.0",
"whatwg-encoding": "^1.0.5",
"whatwg-mimetype": "^2.3.0",
"whatwg-url": "^8.0.0",
"ws": "^7.2.3",
"whatwg-url": "^8.5.0",
"ws": "^7.4.6",
"xml-name-validator": "^3.0.0"
},
"dependencies": {
"acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz",
"integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==",
"dev": true
},
"parse5": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
"dev": true
},
"tough-cookie": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz",
"integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==",
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
"dev": true,
"requires": {
"ip-regex": "^2.1.0",
"psl": "^1.1.28",
"punycode": "^2.1.1"
"debug": "4"
}
},
"form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"https-proxy-agent": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz",
"integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==",
"dev": true,
"requires": {
"agent-base": "6",
"debug": "4"
}
},
"tough-cookie": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz",
"integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==",
"dev": true,
"requires": {
"psl": "^1.1.33",
"punycode": "^2.1.1",
"universalify": "^0.1.2"
}
},
"ws": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz",
"integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz",
"integrity": "sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA==",
"dev": true
}
}
@ -9675,9 +9706,9 @@
"dev": true
},
"json-schema": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
"integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=",
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
"dev": true
},
"json-schema-traverse": {
@ -9728,14 +9759,14 @@
"dev": true
},
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
"integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"dev": true,
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.2.3",
"json-schema": "0.4.0",
"verror": "1.10.0"
}
},
@ -9955,12 +9986,6 @@
"integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=",
"dev": true
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
"integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=",
"dev": true
},
"lodash.uniq": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
@ -12359,8 +12384,7 @@
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"resolved": "",
"dev": true
},
"strip-ansi": {
@ -12565,8 +12589,7 @@
"dependencies": {
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"resolved": "",
"dev": true
},
"ansi-styles": {
@ -13093,26 +13116,6 @@
}
}
},
"request-promise-core": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz",
"integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==",
"dev": true,
"requires": {
"lodash": "^4.17.19"
}
},
"request-promise-native": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz",
"integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==",
"dev": true,
"requires": {
"request-promise-core": "1.1.4",
"stealthy-require": "^1.1.1",
"tough-cookie": "^2.3.3"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -14442,12 +14445,6 @@
"integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=",
"dev": true
},
"stealthy-require": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=",
"dev": true
},
"stream-browserify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
@ -15009,9 +15006,9 @@
}
},
"tr46": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz",
"integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz",
"integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==",
"dev": true,
"requires": {
"punycode": "^2.1.1"
@ -16651,13 +16648,13 @@
"dev": true
},
"whatwg-url": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz",
"integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz",
"integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==",
"dev": true,
"requires": {
"lodash.sortby": "^4.7.0",
"tr46": "^2.0.2",
"lodash": "^4.7.0",
"tr46": "^2.1.0",
"webidl-conversions": "^6.1.0"
}
},
@ -16819,9 +16816,9 @@
}
},
"ws": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz",
"integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz",
"integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==",
"requires": {
"async-limiter": "~1.0.0"
}

View file

@ -0,0 +1,16 @@
import { Person } from "./person";
export interface ChapterMetadata {
id: number;
chapterId: number;
title: string;
year: string;
writers: Array<Person>;
penciller: Array<Person>;
inker: Array<Person>;
colorist: Array<Person>;
letterer: Array<Person>;
coverArtist: Array<Person>;
editor: Array<Person>;
publishers: Array<Person>;
}

View file

@ -1,4 +1,5 @@
import { MangaFile } from './manga-file';
import { Person } from './person';
export interface Chapter {
id: number;
@ -16,4 +17,15 @@ export interface Chapter {
isSpecial: boolean;
title: string;
created: string;
titleName: string;
year: string;
writers: Array<Person>;
penciller: Array<Person>;
inker: Array<Person>;
colorist: Array<Person>;
letterer: Array<Person>;
coverArtist: Array<Person>;
editor: Array<Person>;
publisher: Array<Person>;
}

View file

@ -0,0 +1,4 @@
export interface Genre {
id: number,
title: string;
}

View file

@ -1,7 +1,15 @@
export enum PersonRole {
Other = 0,
Author = 1,
Artist = 2
Other = 1,
Artist = 2,
Writer = 3,
Penciller = 4,
Inker = 5,
Colorist = 6,
Letterer = 7,
CoverArtist = 8,
Editor = 9,
Publisher = 10,
Character = 11
}
export interface Person {

View file

@ -1,10 +1,21 @@
import { CollectionTag } from "./collection-tag";
import { Genre } from "./genre";
import { Person } from "./person";
export interface SeriesMetadata {
publisher: string;
genres: Array<string>;
summary: string;
genres: Array<Genre>;
tags: Array<CollectionTag>;
persons: Array<Person>;
writers: Array<Person>;
artists: Array<Person>;
publishers: Array<Person>;
characters: Array<Person>;
pencillers: Array<Person>;
inkers: Array<Person>;
colorists: Array<Person>;
letterers: Array<Person>;
editors: Array<Person>;
seriesId: number;
}

View file

@ -7,7 +7,7 @@ export interface Series {
originalName: string; // This is not shown to user
localizedName: string;
sortName: string;
summary: string;
//summary: string;
coverImageLocked: boolean;
volumes: Volume[];
pages: number; // Total pages in series

View file

@ -58,7 +58,7 @@ export class ActionService implements OnDestroy {
return;
}
this.libraryService.scan(library?.id).pipe(take(1)).subscribe((res: any) => {
this.toastr.success('Scan started for ' + library.name);
this.toastr.success('Scan queued for ' + library.name);
if (callback) {
callback(library);
}
@ -77,11 +77,14 @@ export class ActionService implements OnDestroy {
}
if (!await this.confirmService.confirm('Refresh metadata will force all cover images and metadata to be recalculated. This is a heavy operation. Are you sure you don\'t want to perform a Scan instead?')) {
if (callback) {
callback(library);
}
return;
}
this.libraryService.refreshMetadata(library?.id).pipe(take(1)).subscribe((res: any) => {
this.toastr.success('Scan started for ' + library.name);
this.toastr.success('Scan queued for ' + library.name);
if (callback) {
callback(library);
}
@ -125,7 +128,7 @@ export class ActionService implements OnDestroy {
*/
scanSeries(series: Series, callback?: SeriesActionCallback) {
this.seriesService.scan(series.libraryId, series.id).pipe(take(1)).subscribe((res: any) => {
this.toastr.success('Scan started for ' + series.name);
this.toastr.success('Scan queued for ' + series.name);
if (callback) {
callback(series);
}

View file

@ -0,0 +1,18 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { environment } from 'src/environments/environment';
import { ChapterMetadata } from '../_models/chapter-metadata';
@Injectable({
providedIn: 'root'
})
export class MetadataService {
baseUrl = environment.apiUrl;
constructor(private httpClient: HttpClient) { }
getChapterMetadata(chapterId: number) {
return this.httpClient.get<ChapterMetadata>(this.baseUrl + 'series/chapter-metadata?chapterId=' + chapterId);
}
}

View file

@ -22,7 +22,6 @@ import { AutocompleteLibModule } from 'angular-ng-autocomplete';
import { ReviewSeriesModalComponent } from './_modals/review-series-modal/review-series-modal.component';
import { CarouselModule } from './carousel/carousel.module';
import { PersonBadgeComponent } from './person-badge/person-badge.component';
import { TypeaheadModule } from './typeahead/typeahead.module';
import { RecentlyAddedComponent } from './recently-added/recently-added.component';
import { OnDeckComponent } from './on-deck/on-deck.component';
@ -33,6 +32,8 @@ import { ReadingListModule } from './reading-list/reading-list.module';
import { SAVER, getSaver } from './shared/_providers/saver.provider';
import { ConfigData } from './_models/config-data';
import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.component';
import { PersonRolePipe } from './person-role.pipe';
import { SeriesMetadataDetailComponent } from './series-metadata-detail/series-metadata-detail.component';
@NgModule({
@ -45,11 +46,12 @@ import { NavEventsToggleComponent } from './nav-events-toggle/nav-events-toggle.
SeriesDetailComponent,
NotConnectedComponent, // Move into ExtrasModule
ReviewSeriesModalComponent,
PersonBadgeComponent,
RecentlyAddedComponent,
OnDeckComponent,
DashboardComponent,
NavEventsToggleComponent,
PersonRolePipe,
SeriesMetadataDetailComponent,
],
imports: [
HttpClientModule,

View file

@ -9,7 +9,12 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body scrollable-modal">
<div class="modal-body scrollable-modal" *ngIf="utilityService.isChapter(data)">
<ng-container *ngIf="utilityService.isChapter(data)">
<app-chapter-metadata-detail [chapter]="data"></app-chapter-metadata-detail>
</ng-container>
</div>
<div class="modal-body scrollable-modal" *ngIf="utilityService.isVolume(data)">
<h4 *ngIf="utilityService.isVolume(data)">Information</h4>
<ng-container *ngIf="utilityService.isVolume(data) || utilityService.isChapter(data)">

View file

@ -69,7 +69,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.editSeriesForm = this.fb.group({
id: new FormControl(this.series.id, []),
summary: new FormControl(this.series.summary, []),
summary: new FormControl('', []),
name: new FormControl(this.series.name, []),
localizedName: new FormControl(this.series.localizedName, []),
sortName: new FormControl(this.series.sortName, []),
@ -88,6 +88,7 @@ export class EditSeriesModalComponent implements OnInit, OnDestroy {
this.metadata = metadata;
this.settings.savedData = metadata.tags;
this.tags = metadata.tags;
this.editSeriesForm.get('summary')?.setValue(this.metadata.summary);
}
});

View file

@ -26,7 +26,7 @@
<div class="card-body" *ngIf="title.length > 0 || actions.length > 0">
<div>
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" ngbTooltip="{{title}}" (click)="handleClick()" tabindex="0">
<span class="card-title" placement="top" id="{{title}}_{{entity.id}}" [ngbTooltip]="tooltipTitle" (click)="handleClick()" tabindex="0">
<span *ngIf="isPromoted()">
<i class="fa fa-angle-double-up" aria-hidden="true"></i>
<span class="sr-only">(promoted)</span>

View file

@ -79,12 +79,18 @@ export class CardItemComponent implements OnInit, OnDestroy {
* Format of the entity (only applies to Series)
*/
format: MangaFormat = MangaFormat.UNKNOWN;
chapterTitle: string = '';
download$: Observable<Download> | null = null;
downloadInProgress: boolean = false;
isShiftDown: boolean = false;
get tooltipTitle() {
if (this.chapterTitle === '') return this.title;
return this.chapterTitle;
}
get MangaFormat(): typeof MangaFormat {
@ -111,6 +117,15 @@ export class CardItemComponent implements OnInit, OnDestroy {
});
}
this.format = (this.entity as Series).format;
if (this.utilityService.isChapter(this.entity)) {
this.chapterTitle = this.utilityService.asChapter(this.entity).titleName;
} else if (this.utilityService.isVolume(this.entity)) {
const vol = this.utilityService.asVolume(this.entity);
if (vol.chapters !== undefined && vol.chapters.length > 0) {
this.chapterTitle = vol.chapters[0].titleName;
}
}
}
ngOnDestroy() {

View file

@ -21,6 +21,8 @@ import { CardDetailsModalComponent } from './_modals/card-details-modal/card-det
import { BulkOperationsComponent } from './bulk-operations/bulk-operations.component';
import { BulkAddToCollectionComponent } from './_modals/bulk-add-to-collection/bulk-add-to-collection.component';
import { PipeModule } from '../pipe/pipe.module';
import { ChapterMetadataDetailComponent } from './chapter-metadata-detail/chapter-metadata-detail.component';
import { FileInfoComponent } from './file-info/file-info.component';
@ -38,7 +40,9 @@ import { PipeModule } from '../pipe/pipe.module';
CardDetailLayoutComponent,
CardDetailsModalComponent,
BulkOperationsComponent,
BulkAddToCollectionComponent
BulkAddToCollectionComponent,
ChapterMetadataDetailComponent,
FileInfoComponent
],
imports: [
CommonModule,
@ -75,7 +79,8 @@ import { PipeModule } from '../pipe/pipe.module';
CardActionablesComponent,
CardDetailLayoutComponent,
CardDetailsModalComponent,
BulkOperationsComponent
BulkOperationsComponent,
ChapterMetadataDetailComponent
]
})
export class CardsModule { }

View file

@ -0,0 +1,113 @@
<ng-container *ngIf="chapter !== undefined">
<div class="container-fluid">
<h4>{{chapter.range}}</h4>
Title: {{chapter.titleName || '-'}}
<!-- Year: {{metadata.year || '-'}} -->
Arc Information
<div class="row no-gutters">
<div class="col">
Id: {{chapter.id}}
</div>
<div class="col">
Pages: {{chapter.pages}}
</div>
</div>
<div class="row no-gutters">
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
Added: {{(chapter.created | date: 'short') || '-'}}
</div>
<div class="col">
Pages: {{chapter.pages}}
</div>
</div>
</div>
<ul class="list-unstyled" >
<li class="media my-4">
<a (click)="readChapter(chapter)" href="javascript:void(0);" title="Read {{libraryType !== LibraryType.Comic ? 'Chapter ' : 'Issue #'}} {{chapter.number}}">
<img class="mr-3" style="width: 74px" [src]="chapter.coverImage">
</a>
<div class="media-body">
<h5 class="mt-0 mb-1">
<span *ngIf="chapter.number !== '0'; else specialHeader">
<!-- <span>
<app-card-actionables (actionHandler)="performAction($event, chapter)" [actions]="chapterActions" [labelBy]="utilityService.formatChapterName(libraryType, true, true) + formatChapterNumber(chapter)"></app-card-actionables>&nbsp;
{{utilityService.formatChapterName(libraryType, true, false) }} {{formatChapterNumber(chapter)}}
</span> -->
<span class="badge badge-primary badge-pill">
<span *ngIf="chapter.pagesRead > 0 && chapter.pagesRead < chapter.pages">{{chapter.pagesRead}} / {{chapter.pages}}</span>
<span *ngIf="chapter.pagesRead === 0">UNREAD</span>
<span *ngIf="chapter.pagesRead === chapter.pages">READ</span>
</span>
</span>
<ng-template #specialHeader>Files</ng-template>
</h5>
<ul class="list-group file-list">
<app-file-info *ngFor="let file of chapter.files" [file]="file" [created]="chapter.created"></app-file-info>
</ul>
<ng-container>
<div class="row no-gutters mt-1" *ngIf="chapter.writers && chapter.writers.length > 0">
<div class="col-md-4">
<h5>Writers</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of chapter.writers" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="chapter.coverArtist && chapter.coverArtist.length > 0">
<div class="col-md-4">
<h5>Artists</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of chapter.coverArtist" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="chapter.publisher && chapter.publisher.length > 0">
<div class="col-md-4">
<h5>Publishers</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of chapter.publisher" [person]="person"></app-person-badge>
</div>
</div>
</ng-container>
</div>
</li>
</ul>
</ng-container>
<!--
<div class="container-fluid" *ngIf="metadata !== undefined">
Chapter {{chapter.range}} {{metadata.title.length > 0 ? ' - ' + metadata.title : ''}}
Title: {{metadata.title || '-'}}
Year: {{metadata.year || '-'}}
Arc Information
<div class="row no-gutters">
<div class="col">
Id: {{chapter.id}}
</div>
<div class="col">
Pages: {{chapter.pages}}
</div>
</div>
<div class="row no-gutters">
<div class="col" *ngIf="chapter.hasOwnProperty('created')">
Added: {{(chapter.created | date: 'short') || '-'}}
</div>
<div class="col">
Pages: {{chapter.pages}}
</div>
</div>
</div> -->

View file

@ -0,0 +1,52 @@
import { Component, Input, OnInit } from '@angular/core';
import { MetadataService } from 'src/app/_services/metadata.service';
import { Chapter } from 'src/app/_models/chapter';
import { ChapterMetadata } from 'src/app/_models/chapter-metadata';
import { UtilityService } from 'src/app/shared/_services/utility.service';
import { LibraryType } from 'src/app/_models/library';
import { ActionItem } from 'src/app/_services/action-factory.service';
@Component({
selector: 'app-chapter-metadata-detail',
templateUrl: './chapter-metadata-detail.component.html',
styleUrls: ['./chapter-metadata-detail.component.scss']
})
export class ChapterMetadataDetailComponent implements OnInit {
@Input() chapter!: Chapter;
@Input() libraryType: LibraryType = LibraryType.Manga;
//metadata!: ChapterMetadata;
get LibraryType(): typeof LibraryType {
return LibraryType;
}
constructor(private metadataService: MetadataService, public utilityService: UtilityService) { }
ngOnInit(): void {
// this.metadataService.getChapterMetadata(this.chapter.id).subscribe(metadata => {
// console.log('Chapter ', this.chapter.number, ' metadata: ', metadata);
// this.metadata = metadata;
// })
}
performAction(action: ActionItem<Chapter>, chapter: Chapter) {
if (typeof action.callback === 'function') {
action.callback(action.action, chapter);
}
}
readChapter(chapter: Chapter) {
// if (chapter.pages === 0) {
// this.toastr.error('There are no pages. Kavita was not able to read this archive.');
// return;
// }
// if (chapter.files.length > 0 && chapter.files[0].format === MangaFormat.EPUB) {
// this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'book', chapter.id]);
// } else {
// this.router.navigate(['library', this.libraryId, 'series', this.seriesId, 'manga', chapter.id]);
// }
}
}

View file

@ -0,0 +1,11 @@
<li class="list-group-item">
<span>{{file.filePath}}</span>
<div class="row no-gutters">
<div class="col">
Pages: {{file.pages}}
</div>
<div class="col" *ngIf="created != undefined">
Added: {{(created | date: 'short') || '-'}}
</div>
</div>
</li>

View file

@ -0,0 +1,25 @@
import { Component, Input, OnInit } from '@angular/core';
import { MangaFile } from 'src/app/_models/manga-file';
@Component({
selector: 'app-file-info',
templateUrl: './file-info.component.html',
styleUrls: ['./file-info.component.scss']
})
export class FileInfoComponent implements OnInit {
/**
* MangaFile to display
*/
@Input() file!: MangaFile;
/**
* DateTime the entity this file belongs to was created
*/
@Input() created: string | undefined = undefined;
constructor() { }
ngOnInit(): void {
}
}

View file

@ -138,7 +138,7 @@ export class SeriesCardComponent implements OnInit, OnChanges, OnDestroy {
async scanLibrary(series: Series) {
this.seriesService.scan(series.libraryId, series.id).subscribe((res: any) => {
this.toastr.success('Scan started for ' + series.name);
this.toastr.success('Scan queued for ' + series.name);
});
}

View file

@ -1,12 +0,0 @@
<div class="badge">
<!-- Put a person image container here -->
<div class="img">
</div>
<div class="">
<ng-content select="[name]"></ng-content>
<div style="font-size: 12px">
<ng-content select="[role]"></ng-content>
</div>
</div>
</div>

View file

@ -0,0 +1,26 @@
import { Pipe, PipeTransform } from '@angular/core';
import { PersonRole } from './_models/person';
@Pipe({
name: 'personRole'
})
export class PersonRolePipe implements PipeTransform {
transform(value: PersonRole): string {
switch (value) {
case PersonRole.Artist: return 'Artist';
case PersonRole.Character: return 'Character';
case PersonRole.Colorist: return 'Colorist';
case PersonRole.CoverArtist: return 'CoverArtist';
case PersonRole.Editor: return 'Editor';
case PersonRole.Inker: return 'Inker';
case PersonRole.Letterer: return 'Letterer';
case PersonRole.Penciller: return 'Penciller';
case PersonRole.Publisher: return 'Publisher';
case PersonRole.Writer: return 'Writer';
case PersonRole.Other: return '';
default: return '';
}
}
}

View file

@ -55,35 +55,8 @@
<app-read-more [text]="seriesSummary" [maxLength]="250"></app-read-more>
</div>
<div *ngIf="seriesMetadata" class="mt-2">
<div class="row no-gutters" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
<div class="col-md-4">
<h5>Genres</h5>
</div>
<div class="col-md-8">
<app-tag-badge *ngFor="let genre of seriesMetadata.genres" [selectionMode]="TagBadgeCursor.Clickable">{{genre}}</app-tag-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
<div class="col-md-4">
<h5>Collections</h5>
</div>
<div class="col-md-8">
<app-tag-badge *ngFor="let tag of seriesMetadata.tags" a11y-click="13,32" class="clickable" routerLink="/collections/{{tag.id}}" [selectionMode]="TagBadgeCursor.Clickable">
{{tag.title}}
</app-tag-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.persons && seriesMetadata.persons.length > 0">
<div class="col-md-4">
<h5>People</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.persons">
<div name>{{person.name}}</div>
<div role>{{person.role}}</div>
</app-person-badge>
</div>
</div>
<app-series-metadata-detail [seriesMetadata]="seriesMetadata"></app-series-metadata-detail>
<div class="row no-gutters mt-1" *ngIf="series.format != MangaFormat.UNKNOWN">
<div class="col-md-4">
<h5>Type</h5>
@ -93,6 +66,8 @@
</div>
</div>
</div>
</div>
</div>
@ -116,12 +91,12 @@
<ng-template ngbNavContent>
<div class="row no-gutters">
<div *ngFor="let volume of volumes; let idx = index; trackBy: trackByVolumeIdentity">
<app-card-item class="col-auto" *ngIf="volume.number != 0" [entity]="volume" [title]="'Volume ' + volume.name" (click)="openVolume(volume)"
<app-card-item class="col-auto" *ngIf="volume.number != 0" [entity]="volume" [title]="formatVolumeTitle(volume)" (click)="openVolume(volume)"
[imageUrl]="imageService.getVolumeCoverImage(volume.id) + '&offset=' + coverImageOffset"
[read]="volume.pagesRead" [total]="volume.pages" [actions]="volumeActions" (selection)="bulkSelectionService.handleCardSelection('volume', idx, volumes.length, $event)" [selected]="bulkSelectionService.isCardSelected('volume', idx)" [allowSelection]="true"></app-card-item>
</div>
<div *ngFor="let chapter of chapters; let idx = index; trackBy: trackByChapterIdentity">
<app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="utilityService.formatChapterName(libraryType, true, true) + chapter.range" (click)="openChapter(chapter)"
<app-card-item class="col-auto" *ngIf="!chapter.isSpecial" [entity]="chapter" [title]="formatChapterTitle(chapter)" (click)="openChapter(chapter)"
[imageUrl]="imageService.getChapterCoverImage(chapter.id) + '&offset=' + coverImageOffset"
[read]="chapter.pagesRead" [total]="chapter.pages" [actions]="chapterActions" (selection)="bulkSelectionService.handleCardSelection('chapter', idx, chapters.length, $event)" [selected]="bulkSelectionService.isCardSelected('chapter', idx)" [allowSelection]="true"></app-card-item>
</div>

View file

@ -3,7 +3,7 @@ import { Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbRatingConfig } from '@ng-bootstrap/ng-bootstrap';
import { ToastrService } from 'ngx-toastr';
import { Subject } from 'rxjs';
import { forkJoin, Subject } from 'rxjs';
import { finalize, take, takeUntil, takeWhile } from 'rxjs/operators';
import { BulkSelectionService } from '../cards/bulk-selection.service';
import { CardDetailsModalComponent } from '../cards/_modals/card-details-modal/card-details-modal.component';
@ -15,6 +15,7 @@ import { DownloadService } from '../shared/_services/download.service';
import { KEY_CODES, UtilityService } from '../shared/_services/utility.service';
import { ReviewSeriesModalComponent } from '../_modals/review-series-modal/review-series-modal.component';
import { Chapter } from '../_models/chapter';
import { RefreshMetadataEvent } from '../_models/events/refresh-metadata-event';
import { ScanSeriesEvent } from '../_models/events/scan-series-event';
import { SeriesRemovedEvent } from '../_models/events/series-removed-event';
import { LibraryType } from '../_models/library';
@ -188,16 +189,21 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
this.toastr.info('This series no longer exists');
this.router.navigateByUrl('/libraries');
}
} else if (event.event === EVENTS.RefreshMetadata) {
const seriesRemovedEvent = event.payload as RefreshMetadataEvent;
if (seriesRemovedEvent.seriesId === this.series.id) {
this.seriesService.getMetadata(this.series.id).pipe(take(1)).subscribe(metadata => {
this.seriesMetadata = metadata;
this.createHTML();
})
}
}
});
const seriesId = parseInt(routeId, 10);
this.libraryId = parseInt(libraryId, 10);
this.seriesImage = this.imageService.getSeriesCoverImage(seriesId);
this.libraryService.getLibraryType(this.libraryId).subscribe(type => {
this.libraryType = type;
this.loadSeries(seriesId);
});
this.loadSeries(seriesId);
}
ngOnDestroy() {
@ -328,11 +334,16 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
loadSeries(seriesId: number) {
this.coverImageOffset = 0;
this.seriesService.getMetadata(seriesId).subscribe(metadata => {
this.seriesMetadata = metadata;
});
this.seriesService.getSeries(seriesId).subscribe(series => {
this.series = series;
forkJoin([
this.libraryService.getLibraryType(this.libraryId),
this.seriesService.getMetadata(seriesId),
this.seriesService.getSeries(seriesId)
]).subscribe(results => {
this.libraryType = results[0];
this.seriesMetadata = results[1];
this.series = results[2];
this.createHTML();
this.titleService.setTitle('Kavita - ' + this.series.name + ' Details');
@ -381,7 +392,10 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
}
createHTML() {
this.seriesSummary = (this.series.summary === null ? '' : this.series.summary).replace(/\n/g, '<br>');
if (this.seriesMetadata !== null) {
this.seriesSummary = (this.seriesMetadata.summary === null ? '' : this.seriesMetadata.summary).replace(/\n/g, '<br>');
}
this.userReview = (this.series.userReview === null ? '' : this.series.userReview).replace(/\n/g, '<br>');
}
@ -566,4 +580,12 @@ export class SeriesDetailComponent implements OnInit, OnDestroy {
})).subscribe(() => {/* No Operation */});;
});
}
formatChapterTitle(chapter: Chapter) {
return this.utilityService.formatChapterName(this.libraryType, true, true) + chapter.range;
}
formatVolumeTitle(volume: Volume) {
return 'Volume ' + volume.name;
}
}

View file

@ -0,0 +1,106 @@
<div class="row no-gutters" *ngIf="seriesMetadata.genres && seriesMetadata.genres.length > 0">
<div class="col-md-4">
<h5>Genres</h5>
</div>
<div class="col-md-8">
<app-tag-badge *ngFor="let genre of seriesMetadata.genres" [selectionMode]="TagBadgeCursor.Clickable">{{genre.title}}</app-tag-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.tags && seriesMetadata.tags.length > 0">
<div class="col-md-4">
<h5>Collections</h5>
</div>
<div class="col-md-8">
<app-tag-badge *ngFor="let tag of seriesMetadata.tags" a11y-click="13,32" class="clickable" routerLink="/collections/{{tag.id}}" [selectionMode]="TagBadgeCursor.Clickable">
{{tag.title}}
</app-tag-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.writers && seriesMetadata.writers.length > 0">
<div class="col-md-4">
<h5>Authors</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.writers" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters">
<hr class="col-md-11">
<a [class.hidden]="hasExtendedProperites" *ngIf="hasExtendedProperites" class="col-md-1 read-more-link" (click)="toggleView()">&nbsp;<i aria-hidden="true" class="fa fa-caret-{{isCollapsed ? 'down' : 'up'}}" aria-controls="extended-series-metadata"></i>&nbsp;See {{isCollapsed ? 'More' : 'Less'}}</a>
</div>
<div #collapse="ngbCollapse" [(ngbCollapse)]="isCollapsed" id="extended-series-metadata">
Stuff
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.artists && seriesMetadata.artists.length > 0">
<div class="col-md-4">
<h5>Artists</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.artists" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.publishers && seriesMetadata.publishers.length > 0">
<div class="col-md-4">
<h5>Publishers</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.publishers" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.characters && seriesMetadata.characters.length > 0">
<div class="col-md-4">
<h5>Characters</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.characters" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.pencillers && seriesMetadata.pencillers.length > 0">
<div class="col-md-4">
<h5>Pencillers</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.pencillers" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.inkers && seriesMetadata.inkers.length > 0">
<div class="col-md-4">
<h5>Inkers</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.inkers" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.colorists && seriesMetadata.colorists.length > 0">
<div class="col-md-4">
<h5>Colorists</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.colorists" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.letterers && seriesMetadata.letterers.length > 0">
<div class="col-md-4">
<h5>Letterers</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.letterers" [person]="person"></app-person-badge>
</div>
</div>
<div class="row no-gutters mt-1" *ngIf="seriesMetadata.editors && seriesMetadata.editors.length > 0">
<div class="col-md-4">
<h5>Editors</h5>
</div>
<div class="col-md-8">
<app-person-badge *ngFor="let person of seriesMetadata.editors" [person]="person"></app-person-badge>
</div>
</div>
</div>

View file

@ -0,0 +1,6 @@
.read-more-link {
font-weight: bold;
text-decoration: none;
color: black;
cursor: pointer;
}

View file

@ -0,0 +1,46 @@
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
import { TagBadgeCursor } from '../shared/tag-badge/tag-badge.component';
import { UtilityService } from '../shared/_services/utility.service';
import { MangaFormat } from '../_models/manga-format';
import { SeriesMetadata } from '../_models/series-metadata';
@Component({
selector: 'app-series-metadata-detail',
templateUrl: './series-metadata-detail.component.html',
styleUrls: ['./series-metadata-detail.component.scss']
})
export class SeriesMetadataDetailComponent implements OnInit, OnChanges {
@Input() seriesMetadata!: SeriesMetadata;
isCollapsed: boolean = false;
hasExtendedProperites: boolean = false;
get MangaFormat(): typeof MangaFormat {
return MangaFormat;
}
get TagBadgeCursor(): typeof TagBadgeCursor {
return TagBadgeCursor;
}
constructor(public utilityService: UtilityService) { }
ngOnChanges(changes: SimpleChanges): void {
this.hasExtendedProperites = this.seriesMetadata.colorists.length > 0 ||
this.seriesMetadata.editors.length > 0 ||
this.seriesMetadata.artists.length > 0 ||
this.seriesMetadata.inkers.length > 0 ||
this.seriesMetadata.letterers.length > 0 ||
this.seriesMetadata.pencillers.length > 0 ||
this.seriesMetadata.publishers.length > 0;
}
ngOnInit(): void {
}
toggleView() {
this.isCollapsed = !this.isCollapsed;
}
}

View file

@ -0,0 +1,20 @@
<!-- <div class="badge">
<div class="col-4 img">
<i class="fa fa-user-circle" aria-hidden="true"></i>
</div>
<div class="col-8">
<span style="font-size: 12px;">{{person.name}}</span>
</div>
</div> -->
<div class="tagbadge cursor">
<div class="media">
<!-- <img src="..." class="align-self-center mr-3" alt="..."> -->
<i class="fa fa-user-circle align-self-center mr-2" aria-hidden="true"></i>
<div class="media-body">
<span class="mt-0 mb-0">{{person.name}}</span>
</div>
</div>
</div>

View file

@ -1,10 +1,10 @@
@use '../../theme/colors';
@use '../../../theme/colors';
$bg-color: #c9c9c9;
$bdr-color: #f2f2f2;
::ng-deep .badge {
background-color: $bg-color;
.badge {
//background-color: $bg-color;
transition: all .3s ease-out;
margin: 3px 5px 3px 0px;
padding: 2px 15px;
@ -21,4 +21,4 @@ $bdr-color: #f2f2f2;
margin-right: 0px;
cursor: pointer;
}
}
}

View file

@ -1,4 +1,5 @@
import { Component, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { Person } from '../../_models/person';
@Component({
selector: 'app-person-badge',
@ -7,6 +8,8 @@ import { Component, OnInit } from '@angular/core';
})
export class PersonBadgeComponent implements OnInit {
@Input() person!: Person;
constructor() { }
ngOnInit(): void {

View file

@ -16,6 +16,7 @@ import { UpdateNotificationModalComponent } from './update-notification/update-n
import { CircularLoaderComponent } from './circular-loader/circular-loader.component';
import { NgCircleProgressModule } from 'ng-circle-progress';
import { SentenceCasePipe } from './sentence-case.pipe';
import { PersonBadgeComponent } from './person-badge/person-badge.component';
@NgModule({
declarations: [
@ -31,6 +32,7 @@ import { SentenceCasePipe } from './sentence-case.pipe';
UpdateNotificationModalComponent,
CircularLoaderComponent,
SentenceCasePipe,
PersonBadgeComponent
],
imports: [
CommonModule,
@ -53,6 +55,7 @@ import { SentenceCasePipe } from './sentence-case.pipe';
SeriesFormatComponent,
TagBadgeComponent,
CircularLoaderComponent,
PersonBadgeComponent
],
})
export class SharedModule { }

View file

@ -16,6 +16,10 @@
}
}
hr {
background-color: colors.$dark-form-border;
}
a.btn {
color: white;
}