mirror of
https://github.com/gotson/komga.git
synced 2025-12-22 00:13:30 +01:00
parent
efdcc98604
commit
5567adc946
10 changed files with 537 additions and 181 deletions
13
komga-webui/package-lock.json
generated
13
komga-webui/package-lock.json
generated
|
|
@ -11389,6 +11389,19 @@
|
|||
"integrity": "sha512-xf88rTeHiXk+XE2Vhi6yj8Wm3gMZrygGdKjJqN8HkV+PwF/t50/LdAKHoHpPcxFAlmQszTZ1CugrK25S7qDRLA==",
|
||||
"dev": true
|
||||
},
|
||||
"language-subtag-registry": {
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz",
|
||||
"integrity": "sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg=="
|
||||
},
|
||||
"language-tags": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz",
|
||||
"integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=",
|
||||
"requires": {
|
||||
"language-subtag-registry": "~0.3.2"
|
||||
}
|
||||
},
|
||||
"launch-editor": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.2.1.tgz",
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
"axios": "^0.19.2",
|
||||
"core-js": "^3.6.5",
|
||||
"jquery": "^3.5.1",
|
||||
"language-tags": "^1.0.5",
|
||||
"lodash": "^4.17.19",
|
||||
"moment": "^2.27.0",
|
||||
"qs": "^6.9.4",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@
|
|||
</v-card-title>
|
||||
|
||||
<v-tabs :vertical="$vuetify.breakpoint.smAndUp" v-model="tab">
|
||||
<v-tab class="justify-start">
|
||||
<v-tab class="justify-start" v-if="single">
|
||||
<v-icon left class="hidden-xs-only">mdi-format-align-center</v-icon>
|
||||
General
|
||||
</v-tab>
|
||||
|
|
@ -32,9 +32,13 @@
|
|||
<v-icon left class="hidden-xs-only">mdi-account-multiple</v-icon>
|
||||
Authors
|
||||
</v-tab>
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
|
||||
Tags
|
||||
</v-tab>
|
||||
|
||||
<!-- Tab: General -->
|
||||
<v-tab-item>
|
||||
<v-tab-item v-if="single">
|
||||
<v-card flat>
|
||||
<v-container fluid>
|
||||
|
||||
|
|
@ -130,52 +134,6 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Publisher -->
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="form.publisher"
|
||||
label="Publisher"
|
||||
filled
|
||||
dense
|
||||
:placeholder="mixed.publisher ? 'MIXED' : ''"
|
||||
@input="$v.form.publisher.$touch()"
|
||||
@change="form.publisherLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.publisherLock ? 'secondary' : ''"
|
||||
@click="form.publisherLock = !form.publisherLock"
|
||||
>
|
||||
{{ form.publisherLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Age Rating -->
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="form.ageRating"
|
||||
label="Age Rating"
|
||||
clearable
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
:placeholder="mixed.ageRating ? 'MIXED' : ''"
|
||||
:error-messages="ageRatingErrors"
|
||||
@input="$v.form.ageRating.$touch()"
|
||||
@blur="$v.form.ageRating.$touch()"
|
||||
@change="form.ageRatingLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.ageRatingLock ? 'secondary' : ''"
|
||||
@click="form.ageRatingLock = !form.ageRatingLock"
|
||||
>
|
||||
{{ form.ageRatingLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Release Date -->
|
||||
<v-row v-if="single">
|
||||
<v-col cols="12">
|
||||
|
|
@ -200,29 +158,6 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Reading Direction -->
|
||||
<v-row>
|
||||
<v-col cols="">
|
||||
<v-select v-model="form.readingDirection"
|
||||
:items="readingDirections"
|
||||
label="Reading Direction"
|
||||
clearable
|
||||
filled
|
||||
:placeholder="mixed.readingDirection ? 'MIXED' : ''"
|
||||
@input="$v.form.readingDirection.$touch()"
|
||||
@change="form.readingDirectionLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.readingDirectionLock ? 'secondary' : ''"
|
||||
@click="form.readingDirectionLock = !form.readingDirectionLock"
|
||||
>
|
||||
{{ form.readingDirectionLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
|
@ -270,6 +205,47 @@
|
|||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Tab: Tags -->
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-container fluid>
|
||||
<v-alert v-if="!single"
|
||||
type="warning"
|
||||
outlined
|
||||
dense
|
||||
>
|
||||
You are editing tags for multiple books. This will override existing tags of each book.
|
||||
</v-alert>
|
||||
|
||||
<!-- Tags -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<span class="text-body-2">Tags</span>
|
||||
<v-combobox v-model="form.tags"
|
||||
:items="tagsAvailable"
|
||||
@input="$v.form.tags.$touch()"
|
||||
@change="form.tagsLock = true"
|
||||
hide-selected
|
||||
chips
|
||||
deletable-chips
|
||||
multiple
|
||||
filled
|
||||
dense
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.tagsLock ? 'secondary' : ''"
|
||||
@click="form.tagsLock = !form.tagsLock"
|
||||
>
|
||||
{{ form.tagsLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
</v-tabs>
|
||||
|
||||
<v-card-actions class="hidden-xs-only">
|
||||
|
|
@ -300,10 +276,9 @@
|
|||
<script lang="ts">
|
||||
import { groupAuthorsByRole } from '@/functions/authors'
|
||||
import { authorRoles } from '@/types/author-roles'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
import moment from 'moment'
|
||||
import Vue from 'vue'
|
||||
import { helpers, minValue, requiredIf } from 'vuelidate/lib/validators'
|
||||
import { helpers, requiredIf } from 'vuelidate/lib/validators'
|
||||
|
||||
const validDate = (value: any) => !helpers.req(value) || moment(value, 'YYYY-MM-DD', true).isValid()
|
||||
|
||||
|
|
@ -324,25 +299,17 @@ export default Vue.extend({
|
|||
numberLock: false,
|
||||
numberSort: 0,
|
||||
numberSortLock: false,
|
||||
readingDirection: '',
|
||||
readingDirectionLock: false,
|
||||
publisher: '',
|
||||
publisherLock: false,
|
||||
ageRating: undefined as unknown as number,
|
||||
ageRatingLock: false,
|
||||
releaseDate: '',
|
||||
releaseDateLock: false,
|
||||
authors: {},
|
||||
authorsLock: false,
|
||||
},
|
||||
mixed: {
|
||||
readingDirection: false,
|
||||
publisher: false,
|
||||
ageRating: false,
|
||||
tags: [] as string[],
|
||||
tagsLock: false,
|
||||
},
|
||||
authorRoles,
|
||||
authorSearch: [],
|
||||
authorSearchResults: [] as string[],
|
||||
tagsAvailable: [] as string[],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
|
@ -390,26 +357,19 @@ export default Vue.extend({
|
|||
return this.single
|
||||
}),
|
||||
},
|
||||
ageRating: { minValue: minValue(0) },
|
||||
tags: {},
|
||||
releaseDate: { validDate },
|
||||
summary: {},
|
||||
readingDirection: {},
|
||||
publisher: {},
|
||||
authors: {},
|
||||
},
|
||||
},
|
||||
async created () {
|
||||
this.tagsAvailable = await this.$komgaReferential.getTags()
|
||||
},
|
||||
computed: {
|
||||
single (): boolean {
|
||||
return !Array.isArray(this.books)
|
||||
},
|
||||
readingDirections (): any[] {
|
||||
return Object.keys(ReadingDirection).map(x => (
|
||||
{
|
||||
text: this.$_.capitalize(x.replace(/_/g, ' ')),
|
||||
value: x,
|
||||
}),
|
||||
)
|
||||
},
|
||||
authorSearchResultsFull (): string[] {
|
||||
// merge local values with server search, so that already input value is available
|
||||
const local = (this.$_.values(this.form.authors).flat()) as unknown as string[]
|
||||
|
|
@ -420,12 +380,6 @@ export default Vue.extend({
|
|||
? `Edit ${this.$_.get(this.books, 'metadata.title')}`
|
||||
: `Edit ${(this.books as BookDto[]).length} books`
|
||||
},
|
||||
ageRatingErrors (): string[] {
|
||||
const errors = [] as string[]
|
||||
if (!this.$v.form?.ageRating?.$dirty) return errors
|
||||
!this.$v?.form?.ageRating?.minValue && errors.push('Age rating must be 0 or more')
|
||||
return errors
|
||||
},
|
||||
releaseDateErrors (): string[] {
|
||||
const errors = [] as string[]
|
||||
if (!this.$v.form?.releaseDate?.$dirty) return errors
|
||||
|
|
@ -438,7 +392,7 @@ export default Vue.extend({
|
|||
const errors = [] as string[]
|
||||
const formField = this.$v.form!![fieldName] as any
|
||||
if (!formField.$dirty) return errors
|
||||
!formField.required && errors.push('Required')
|
||||
!formField.required && errors.push('Requiredb')
|
||||
return errors
|
||||
},
|
||||
dialogReset (books: BookDto | BookDto[]) {
|
||||
|
|
@ -447,32 +401,17 @@ export default Vue.extend({
|
|||
if (Array.isArray(books) && books.length === 0) return
|
||||
else if (this.$_.isEmpty(books)) return
|
||||
if (Array.isArray(books) && books.length > 0) {
|
||||
const readingDirection = this.$_.uniq(books.map(x => x.metadata.readingDirection))
|
||||
this.form.readingDirection = readingDirection.length > 1 ? '' : readingDirection[0]
|
||||
this.mixed.readingDirection = readingDirection.length > 1
|
||||
|
||||
const readingDirectionLock = this.$_.uniq(books.map(x => x.metadata.readingDirectionLock))
|
||||
this.form.readingDirectionLock = readingDirectionLock.length > 1 ? false : readingDirectionLock[0]
|
||||
|
||||
const ageRating = this.$_.uniq(books.map(x => x.metadata.ageRating))
|
||||
this.form.ageRating = ageRating.length > 1 ? undefined as unknown as number : ageRating[0]
|
||||
this.mixed.ageRating = ageRating.length > 1
|
||||
|
||||
const ageRatingLock = this.$_.uniq(books.map(x => x.metadata.ageRatingLock))
|
||||
this.form.ageRatingLock = ageRatingLock.length > 1 ? false : ageRatingLock[0]
|
||||
|
||||
const publisher = this.$_.uniq(books.map(x => x.metadata.publisher))
|
||||
this.form.publisher = publisher.length > 1 ? '' : publisher[0]
|
||||
this.mixed.publisher = publisher.length > 1
|
||||
|
||||
const publisherLock = this.$_.uniq(books.map(x => x.metadata.publisherLock))
|
||||
this.form.publisherLock = publisherLock.length > 1 ? false : publisherLock[0]
|
||||
|
||||
this.form.authors = {}
|
||||
|
||||
const authorsLock = this.$_.uniq(books.map(x => x.metadata.authorsLock))
|
||||
this.form.authorsLock = authorsLock.length > 1 ? false : authorsLock[0]
|
||||
|
||||
this.form.tags = []
|
||||
|
||||
const tagsLock = this.$_.uniq(books.map(x => x.metadata.tagsLock))
|
||||
this.form.tagsLock = tagsLock.length > 1 ? false : tagsLock[0]
|
||||
} else {
|
||||
this.form.tags = []
|
||||
const book = books as BookDto
|
||||
this.$_.merge(this.form, book.metadata)
|
||||
this.form.authors = groupAuthorsByRole(book.metadata.authors)
|
||||
|
|
@ -494,22 +433,8 @@ export default Vue.extend({
|
|||
validateForm (): any {
|
||||
if (!this.$v.$invalid) {
|
||||
const metadata = {
|
||||
readingDirectionLock: this.form.readingDirectionLock,
|
||||
ageRatingLock: this.form.ageRatingLock,
|
||||
publisherLock: this.form.publisherLock,
|
||||
authorsLock: this.form.authorsLock,
|
||||
}
|
||||
|
||||
if (this.$v.form?.readingDirection?.$dirty) {
|
||||
this.$_.merge(metadata, { readingDirection: this.form.readingDirection ? this.form.readingDirection : null })
|
||||
}
|
||||
|
||||
if (this.$v.form?.ageRating?.$dirty) {
|
||||
this.$_.merge(metadata, { ageRating: this.form.ageRating })
|
||||
}
|
||||
|
||||
if (this.$v.form?.publisher?.$dirty) {
|
||||
this.$_.merge(metadata, { publisher: this.form.publisher })
|
||||
tagsLock: this.form.tagsLock,
|
||||
}
|
||||
|
||||
if (this.$v.form?.authors?.$dirty) {
|
||||
|
|
@ -520,6 +445,10 @@ export default Vue.extend({
|
|||
})
|
||||
}
|
||||
|
||||
if (this.$v.form?.tags?.$dirty) {
|
||||
this.$_.merge(metadata, { tags: this.form.tags })
|
||||
}
|
||||
|
||||
if (this.single) {
|
||||
this.$_.merge(metadata, {
|
||||
titleLock: this.form.titleLock,
|
||||
|
|
|
|||
|
|
@ -23,13 +23,17 @@
|
|||
{{ dialogTitle }}
|
||||
</v-card-title>
|
||||
|
||||
<v-tabs :vertical="$vuetify.breakpoint.smAndUp">
|
||||
<v-tabs :vertical="$vuetify.breakpoint.smAndUp" v-model="tab">
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-format-align-center</v-icon>
|
||||
General
|
||||
</v-tab>
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
|
||||
Tags
|
||||
</v-tab>
|
||||
|
||||
<!-- General -->
|
||||
<!-- Tab: General -->
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-container fluid>
|
||||
|
|
@ -80,9 +84,30 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Status -->
|
||||
<!-- Summary -->
|
||||
<v-row v-if="single">
|
||||
<v-col cols="12">
|
||||
<v-textarea v-model="form.summary"
|
||||
label="Summary"
|
||||
filled
|
||||
dense
|
||||
@input="$v.form.summary.$touch()"
|
||||
@change="form.summaryLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.summaryLock ? 'secondary' : ''"
|
||||
@click="form.summaryLock = !form.summaryLock"
|
||||
>
|
||||
{{ form.summaryLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<!-- Status -->
|
||||
<v-col cols="6">
|
||||
<v-select :items="seriesStatus"
|
||||
v-model="form.status"
|
||||
label="Status"
|
||||
|
|
@ -100,11 +125,168 @@
|
|||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
|
||||
<!-- Language -->
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="form.language"
|
||||
label="Language"
|
||||
filled
|
||||
dense
|
||||
:placeholder="mixed.language ? 'MIXED' : ''"
|
||||
:error-messages="languageErrors"
|
||||
@input="$v.form.language.$touch()"
|
||||
@change="form.languageLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.languageLock ? 'secondary' : ''"
|
||||
@click="form.languageLock = !form.languageLock"
|
||||
>
|
||||
{{ form.languageLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Reading Direction -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-select v-model="form.readingDirection"
|
||||
:items="readingDirections"
|
||||
label="Reading Direction"
|
||||
clearable
|
||||
filled
|
||||
:placeholder="mixed.readingDirection ? 'MIXED' : ''"
|
||||
@input="$v.form.readingDirection.$touch()"
|
||||
@change="form.readingDirectionLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.readingDirectionLock ? 'secondary' : ''"
|
||||
@click="form.readingDirectionLock = !form.readingDirectionLock"
|
||||
>
|
||||
{{ form.readingDirectionLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<!-- Publisher -->
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="form.publisher"
|
||||
label="Publisher"
|
||||
filled
|
||||
dense
|
||||
:placeholder="mixed.publisher ? 'MIXED' : ''"
|
||||
@input="$v.form.publisher.$touch()"
|
||||
@change="form.publisherLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.publisherLock ? 'secondary' : ''"
|
||||
@click="form.publisherLock = !form.publisherLock"
|
||||
>
|
||||
{{ form.publisherLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<!-- Age Rating -->
|
||||
<v-col cols="6">
|
||||
<v-text-field v-model="form.ageRating"
|
||||
label="Age Rating"
|
||||
clearable
|
||||
filled
|
||||
dense
|
||||
type="number"
|
||||
:placeholder="mixed.ageRating ? 'MIXED' : ''"
|
||||
:error-messages="ageRatingErrors"
|
||||
@input="$v.form.ageRating.$touch()"
|
||||
@blur="$v.form.ageRating.$touch()"
|
||||
@change="form.ageRatingLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.ageRatingLock ? 'secondary' : ''"
|
||||
@click="form.ageRatingLock = !form.ageRatingLock"
|
||||
>
|
||||
{{ form.ageRatingLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Tab: Tags -->
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-container fluid>
|
||||
<v-alert v-if="!single"
|
||||
type="warning"
|
||||
outlined
|
||||
dense
|
||||
>
|
||||
You are editing tags for multiple series. This will override existing tags of each series.
|
||||
</v-alert>
|
||||
|
||||
<!-- Genres -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<span class="text-body-2">Genres</span>
|
||||
<v-combobox v-model="form.genres"
|
||||
:items="genresAvailable"
|
||||
@input="$v.form.genres.$touch()"
|
||||
@change="form.genresLock = true"
|
||||
hide-selected
|
||||
chips
|
||||
deletable-chips
|
||||
multiple
|
||||
filled
|
||||
dense
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.genresLock ? 'secondary' : ''"
|
||||
@click="form.genresLock = !form.genresLock"
|
||||
>
|
||||
{{ form.genresLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Tags -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<span class="text-body-2">Tags</span>
|
||||
<v-combobox v-model="form.tags"
|
||||
:items="tagsAvailable"
|
||||
@input="$v.form.tags.$touch()"
|
||||
@change="form.tagsLock = true"
|
||||
hide-selected
|
||||
chips
|
||||
deletable-chips
|
||||
multiple
|
||||
filled
|
||||
dense
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.tagsLock ? 'secondary' : ''"
|
||||
@click="form.tagsLock = !form.tagsLock"
|
||||
>
|
||||
{{ form.tagsLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
</v-combobox>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-actions class="hidden-xs-only">
|
||||
|
|
@ -135,7 +317,12 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { SeriesStatus } from '@/types/enum-series'
|
||||
import { requiredIf } from 'vuelidate/lib/validators'
|
||||
import { helpers, minValue, requiredIf } from 'vuelidate/lib/validators'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
|
||||
const tags = require('language-tags')
|
||||
|
||||
const validLanguage = (value: string) => !helpers.req(value) || tags.check(value)
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'EditSeriesDialog',
|
||||
|
|
@ -144,6 +331,7 @@ export default Vue.extend({
|
|||
modal: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
tab: 0,
|
||||
form: {
|
||||
status: '',
|
||||
statusLock: false,
|
||||
|
|
@ -151,10 +339,30 @@ export default Vue.extend({
|
|||
titleLock: false,
|
||||
titleSort: '',
|
||||
titleSortLock: false,
|
||||
summary: '',
|
||||
summaryLock: false,
|
||||
readingDirection: '',
|
||||
readingDirectionLock: false,
|
||||
publisher: '',
|
||||
publisherLock: false,
|
||||
ageRating: undefined as unknown as number,
|
||||
ageRatingLock: false,
|
||||
language: '',
|
||||
languageLock: false,
|
||||
genres: [],
|
||||
genresLock: false,
|
||||
tags: [],
|
||||
tagsLock: false,
|
||||
},
|
||||
mixed: {
|
||||
status: false,
|
||||
readingDirection: false,
|
||||
publisher: false,
|
||||
ageRating: false,
|
||||
language: false,
|
||||
},
|
||||
genresAvailable: [] as string[],
|
||||
tagsAvailable: [] as string[],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
|
@ -192,18 +400,50 @@ export default Vue.extend({
|
|||
return this.single
|
||||
}),
|
||||
},
|
||||
summary: {},
|
||||
language: {
|
||||
validLanguage: validLanguage,
|
||||
},
|
||||
genres: {},
|
||||
tags: {},
|
||||
ageRating: { minValue: minValue(0) },
|
||||
readingDirection: {},
|
||||
publisher: {},
|
||||
},
|
||||
},
|
||||
async created () {
|
||||
this.genresAvailable = await this.$komgaReferential.getGenres()
|
||||
this.tagsAvailable = await this.$komgaReferential.getTags()
|
||||
},
|
||||
computed: {
|
||||
single (): boolean {
|
||||
return !Array.isArray(this.series)
|
||||
},
|
||||
readingDirections (): any[] {
|
||||
return Object.keys(ReadingDirection).map(x => (
|
||||
{
|
||||
text: this.$_.capitalize(x.replace(/_/g, ' ')),
|
||||
value: x,
|
||||
}),
|
||||
)
|
||||
},
|
||||
seriesStatus (): any[] {
|
||||
return Object.keys(SeriesStatus).map(x => ({
|
||||
text: this.$_.capitalize(x),
|
||||
value: x,
|
||||
}))
|
||||
},
|
||||
ageRatingErrors (): string[] {
|
||||
const errors = [] as string[]
|
||||
if (!this.$v.form?.ageRating?.$dirty) return errors
|
||||
!this.$v?.form?.ageRating?.minValue && errors.push('Age rating must be 0 or more')
|
||||
return errors
|
||||
},
|
||||
languageErrors (): string[] {
|
||||
if (!this.$v.form?.language?.$dirty) return []
|
||||
if (!this.$v?.form?.language?.validLanguage) return tags(this.form.language).errors().map((x: any) => x.message)
|
||||
return []
|
||||
},
|
||||
dialogTitle (): string {
|
||||
return this.single
|
||||
? `Edit ${this.$_.get(this.series, 'metadata.title')}`
|
||||
|
|
@ -219,6 +459,7 @@ export default Vue.extend({
|
|||
return errors
|
||||
},
|
||||
dialogReset (series: SeriesDto | SeriesDto[]) {
|
||||
this.tab = 0
|
||||
this.$v.$reset()
|
||||
if (Array.isArray(series) && series.length === 0) return
|
||||
if (Array.isArray(series) && series.length > 0) {
|
||||
|
|
@ -228,7 +469,47 @@ export default Vue.extend({
|
|||
|
||||
const statusLock = this.$_.uniq(series.map(x => x.metadata.statusLock))
|
||||
this.form.statusLock = statusLock.length > 1 ? false : statusLock[0]
|
||||
|
||||
const readingDirection = this.$_.uniq(series.map(x => x.metadata.readingDirection))
|
||||
this.form.readingDirection = readingDirection.length > 1 ? '' : readingDirection[0]
|
||||
this.mixed.readingDirection = readingDirection.length > 1
|
||||
|
||||
const readingDirectionLock = this.$_.uniq(series.map(x => x.metadata.readingDirectionLock))
|
||||
this.form.readingDirectionLock = readingDirectionLock.length > 1 ? false : readingDirectionLock[0]
|
||||
|
||||
const ageRating = this.$_.uniq(series.map(x => x.metadata.ageRating))
|
||||
this.form.ageRating = ageRating.length > 1 ? undefined as unknown as number : ageRating[0]
|
||||
this.mixed.ageRating = ageRating.length > 1
|
||||
|
||||
const ageRatingLock = this.$_.uniq(series.map(x => x.metadata.ageRatingLock))
|
||||
this.form.ageRatingLock = ageRatingLock.length > 1 ? false : ageRatingLock[0]
|
||||
|
||||
const publisher = this.$_.uniq(series.map(x => x.metadata.publisher))
|
||||
this.form.publisher = publisher.length > 1 ? '' : publisher[0]
|
||||
this.mixed.publisher = publisher.length > 1
|
||||
|
||||
const publisherLock = this.$_.uniq(series.map(x => x.metadata.publisherLock))
|
||||
this.form.publisherLock = publisherLock.length > 1 ? false : publisherLock[0]
|
||||
|
||||
const language = this.$_.uniq(series.map(x => x.metadata.language))
|
||||
this.form.language = language.length > 1 ? '' : language[0]
|
||||
this.mixed.language = language.length > 1
|
||||
|
||||
const languageLock = this.$_.uniq(series.map(x => x.metadata.languageLock))
|
||||
this.form.languageLock = languageLock.length > 1 ? false : languageLock[0]
|
||||
|
||||
this.form.genres = []
|
||||
|
||||
const genresLock = this.$_.uniq(series.map(x => x.metadata.genresLock))
|
||||
this.form.genresLock = genresLock.length > 1 ? false : genresLock[0]
|
||||
|
||||
this.form.tags = []
|
||||
|
||||
const tagsLock = this.$_.uniq(series.map(x => x.metadata.tagsLock))
|
||||
this.form.tagsLock = tagsLock.length > 1 ? false : tagsLock[0]
|
||||
} else {
|
||||
this.form.genres = []
|
||||
this.form.tags = []
|
||||
this.$_.merge(this.form, (series as SeriesDto).metadata)
|
||||
}
|
||||
},
|
||||
|
|
@ -249,16 +530,47 @@ export default Vue.extend({
|
|||
if (!this.$v.$invalid) {
|
||||
const metadata = {
|
||||
statusLock: this.form.statusLock,
|
||||
readingDirectionLock: this.form.readingDirectionLock,
|
||||
ageRatingLock: this.form.ageRatingLock,
|
||||
publisherLock: this.form.publisherLock,
|
||||
languageLock: this.form.languageLock,
|
||||
genresLock: this.form.genresLock,
|
||||
tagsLock: this.form.tagsLock,
|
||||
}
|
||||
|
||||
if (this.$v.form?.status?.$dirty) {
|
||||
this.$_.merge(metadata, { status: this.form.status })
|
||||
}
|
||||
|
||||
if (this.$v.form?.readingDirection?.$dirty) {
|
||||
this.$_.merge(metadata, { readingDirection: this.form.readingDirection ? this.form.readingDirection : null })
|
||||
}
|
||||
|
||||
if (this.$v.form?.ageRating?.$dirty) {
|
||||
this.$_.merge(metadata, { ageRating: this.form.ageRating })
|
||||
}
|
||||
|
||||
if (this.$v.form?.publisher?.$dirty) {
|
||||
this.$_.merge(metadata, { publisher: this.form.publisher })
|
||||
}
|
||||
|
||||
if (this.$v.form?.genres?.$dirty) {
|
||||
this.$_.merge(metadata, { genres: this.form.genres })
|
||||
}
|
||||
|
||||
if (this.$v.form?.tags?.$dirty) {
|
||||
this.$_.merge(metadata, { tags: this.form.tags })
|
||||
}
|
||||
|
||||
if (this.$v.form?.language?.$dirty) {
|
||||
this.$_.merge(metadata, { language: this.form.language })
|
||||
}
|
||||
|
||||
if (this.single) {
|
||||
this.$_.merge(metadata, {
|
||||
titleLock: this.form.titleLock,
|
||||
titleSortLock: this.form.titleSortLock,
|
||||
summaryLock: this.form.summaryLock,
|
||||
})
|
||||
|
||||
if (this.$v.form?.title?.$dirty) {
|
||||
|
|
@ -268,6 +580,10 @@ export default Vue.extend({
|
|||
if (this.$v.form?.titleSort?.$dirty) {
|
||||
this.$_.merge(metadata, { titleSort: this.form.titleSort })
|
||||
}
|
||||
|
||||
if (this.$v.form?.summary?.$dirty) {
|
||||
this.$_.merge(metadata, { summary: this.form.summary })
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
|
|
|||
|
|
@ -27,4 +27,28 @@ export default class KomgaReferentialService {
|
|||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getGenres (): Promise<string[]> {
|
||||
try {
|
||||
return (await this.http.get('/api/v1/genres')).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve genres'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getTags (): Promise<string[]> {
|
||||
try {
|
||||
return (await this.http.get('/api/v1/tags')).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve tags'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,16 +48,12 @@ interface BookMetadataDto {
|
|||
numberLock: boolean,
|
||||
numberSort: number,
|
||||
numberSortLock: boolean,
|
||||
readingDirection: string,
|
||||
readingDirectionLock: boolean,
|
||||
publisher: string,
|
||||
publisherLock: boolean,
|
||||
ageRating: number,
|
||||
ageRatingLock: boolean,
|
||||
releaseDate: string,
|
||||
releaseDateLock: boolean,
|
||||
authors: AuthorDto[],
|
||||
authorsLock: boolean,
|
||||
tags: String[],
|
||||
tagsLock: boolean
|
||||
}
|
||||
|
||||
interface ReadProgressDto {
|
||||
|
|
@ -76,16 +72,12 @@ interface BookMetadataUpdateDto {
|
|||
numberLock?: boolean,
|
||||
numberSort?: number,
|
||||
numberSortLock?: boolean,
|
||||
readingDirection?: string,
|
||||
readingDirectionLock?: boolean,
|
||||
publisher?: string,
|
||||
publisherLock?: boolean,
|
||||
ageRating?: number,
|
||||
ageRatingLock?: boolean,
|
||||
releaseDate?: string,
|
||||
releaseDateLock?: boolean,
|
||||
authors?: AuthorDto[],
|
||||
authorsLock?: boolean,
|
||||
tags?: String[],
|
||||
tagsLock?: boolean
|
||||
}
|
||||
|
||||
interface AuthorDto {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,21 @@ interface SeriesMetadata {
|
|||
title: string,
|
||||
titleLock: boolean,
|
||||
titleSort: string,
|
||||
titleSortLock: boolean
|
||||
titleSortLock: boolean,
|
||||
summary: string,
|
||||
summaryLock: boolean,
|
||||
readingDirection: string,
|
||||
readingDirectionLock: boolean,
|
||||
publisher: string,
|
||||
publisherLock: boolean,
|
||||
ageRating: number,
|
||||
ageRatingLock: boolean,
|
||||
language: string,
|
||||
languageLock: boolean,
|
||||
genres: string[],
|
||||
genresLock: boolean,
|
||||
tags: String[],
|
||||
tagsLock: boolean
|
||||
}
|
||||
|
||||
interface SeriesMetadataUpdateDto {
|
||||
|
|
@ -28,5 +42,19 @@ interface SeriesMetadataUpdateDto {
|
|||
title?: string,
|
||||
titleLock?: boolean,
|
||||
titleSort?: string,
|
||||
titleSortLock?: boolean
|
||||
titleSortLock?: boolean,
|
||||
summary?: string,
|
||||
summaryLock?: boolean,
|
||||
readingDirection?: string,
|
||||
readingDirectionLock?: boolean,
|
||||
publisher?: string,
|
||||
publisherLock?: boolean,
|
||||
ageRating?: number,
|
||||
ageRatingLock?: boolean,
|
||||
language?: string,
|
||||
languageLock?: boolean,
|
||||
genres?: string[],
|
||||
genresLock?: boolean,
|
||||
tags?: String[],
|
||||
tagsLock?: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@
|
|||
timeout="3000"
|
||||
>
|
||||
<p class="text-body-1 text-center ma-0">
|
||||
{{ readingDirectionText }}{{ notificationReadingDirection.fromMetadata ? ' (from book metadata)' : '' }}
|
||||
{{ readingDirectionText }}{{ notificationReadingDirection.fromMetadata ? ' (from series metadata)' : '' }}
|
||||
</p>
|
||||
</v-snackbar>
|
||||
|
||||
|
|
@ -576,9 +576,9 @@ export default Vue.extend({
|
|||
}
|
||||
|
||||
// set non-persistent reading direction if exists in metadata
|
||||
if (this.book.metadata.readingDirection in ReadingDirection && this.readingDirection !== this.book.metadata.readingDirection) {
|
||||
if (this.series.metadata.readingDirection in ReadingDirection && this.readingDirection !== this.series.metadata.readingDirection) {
|
||||
// bypass setter so cookies aren't set
|
||||
this.settings.readingDirection = this.book.metadata.readingDirection as ReadingDirection
|
||||
this.settings.readingDirection = this.series.metadata.readingDirection as ReadingDirection
|
||||
this.sendNotificationReadingDirection(true)
|
||||
} else {
|
||||
this.sendNotificationReadingDirection(false)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,6 @@
|
|||
<v-row class="text-body-2">
|
||||
<v-col>
|
||||
<span class="mr-3">#{{ book.metadata.number }}</span>
|
||||
<badge v-if="book.metadata.ageRating">{{ book.metadata.ageRating }}+</badge>
|
||||
</v-col>
|
||||
<v-col cols="auto" v-if="book.metadata.releaseDate">
|
||||
{{ book.metadata.releaseDate | moment('MMMM DD, YYYY') }}
|
||||
|
|
@ -95,11 +94,6 @@
|
|||
|
||||
<v-divider/>
|
||||
|
||||
<v-row class="text-body-2" v-if="book.metadata.publisher">
|
||||
<v-col cols="6" sm="4" md="2">PUBLISHER</v-col>
|
||||
<v-col>{{ book.metadata.publisher }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="text-body-2"
|
||||
v-for="(names, key) in authorsByRole"
|
||||
:key="key"
|
||||
|
|
@ -121,6 +115,20 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.tags.length > 0">
|
||||
<v-col cols="6" sm="4" md="2" class="text-body-2 py-1">TAGS</v-col>
|
||||
<v-col class="text-body-2 text-capitalize py-1">
|
||||
<v-chip v-for="(t, i) in book.metadata.tags"
|
||||
:key="i"
|
||||
class="mr-2"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
>{{ t }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="$vuetify.breakpoint.name !== 'xs'">
|
||||
<v-col>
|
||||
<read-lists-expansion-panels :read-lists="readLists"/>
|
||||
|
|
@ -181,12 +189,6 @@
|
|||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="book.metadata.readingDirection">
|
||||
<v-col cols="2" md="1" lg="1" xl="1" class="text-body-2">READING DIRECTION</v-col>
|
||||
<v-col cols="10" class="text-body-2">{{ readingDirection }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col cols="2" md="1" lg="1" xl="1" class="text-body-2">FILE</v-col>
|
||||
<v-col cols="10" class="text-body-2">{{ book.url }}</v-col>
|
||||
|
|
@ -198,7 +200,6 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Badge from '@/components/Badge.vue'
|
||||
import BookActionsMenu from '@/components/menus/BookActionsMenu.vue'
|
||||
import ItemCard from '@/components/ItemCard.vue'
|
||||
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
|
||||
|
|
@ -214,7 +215,7 @@ import ReadListsExpansionPanels from '@/components/ReadListsExpansionPanels.vue'
|
|||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseBook',
|
||||
components: { ToolbarSticky, Badge, ItemCard, BookActionsMenu, ReadListsExpansionPanels },
|
||||
components: { ToolbarSticky, ItemCard, BookActionsMenu, ReadListsExpansionPanels },
|
||||
data: () => {
|
||||
return {
|
||||
book: {} as BookDto,
|
||||
|
|
@ -281,9 +282,6 @@ export default Vue.extend({
|
|||
readProgressPercentage (): number {
|
||||
return getReadProgressPercentage(this.book)
|
||||
},
|
||||
readingDirection (): string {
|
||||
return this.$_.capitalize(this.book.metadata.readingDirection.replace(/_/g, ' '))
|
||||
},
|
||||
previousId (): string {
|
||||
return this.siblingPrevious?.id?.toString() || '0'
|
||||
},
|
||||
|
|
|
|||
|
|
@ -63,17 +63,66 @@
|
|||
></item-card>
|
||||
|
||||
</v-col>
|
||||
<v-col cols="8">
|
||||
<v-col cols="8" v-if="series.metadata">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-h5" v-if="$_.get(series, 'metadata.title')">{{ series.metadata.title }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="text-body-2">
|
||||
<v-col>
|
||||
<v-chip label small>{{ series.metadata.status }}</v-chip>
|
||||
<v-chip label small v-if="series.metadata.ageRating" class="ml-2">{{
|
||||
series.metadata.ageRating
|
||||
}}+
|
||||
</v-chip>
|
||||
<v-chip label small v-if="series.metadata.language" class="ml-2">{{ languageDisplay }}</v-chip>
|
||||
<v-chip label small v-if="series.metadata.readingDirection" class="ml-2">{{ readingDirection }}</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mt-3">
|
||||
<v-col>
|
||||
<div class="text-body-1"
|
||||
style="white-space: pre-wrap"
|
||||
>{{ series.metadata.summary }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="auto" class="text-body-2">STATUS</v-col>
|
||||
<v-col cols="auto" class="text-body-2 text-capitalize" v-if="series.metadata">{{
|
||||
series.metadata.status.toLowerCase() }}
|
||||
<v-col cols="6" sm="4" md="2" class="text-body-2 py-1">PUBLISHER</v-col>
|
||||
<v-col class="text-body-2 text-capitalize py-1" v-if="series.metadata.publisher">
|
||||
{{ series.metadata.publisher }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="series.metadata.genres.length > 0">
|
||||
<v-col cols="6" sm="4" md="2" class="text-body-2 py-1">GENRE</v-col>
|
||||
<v-col class="text-body-2 text-capitalize py-1">
|
||||
<v-chip v-for="(t, i) in series.metadata.genres"
|
||||
:key="i"
|
||||
class="mr-2"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
>{{ t }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="series.metadata.tags.length > 0">
|
||||
<v-col cols="6" sm="4" md="2" class="text-body-2 py-1">TAGS</v-col>
|
||||
<v-col class="text-body-2 text-capitalize py-1">
|
||||
<v-chip v-for="(t, i) in series.metadata.tags"
|
||||
:key="i"
|
||||
class="mr-2"
|
||||
label
|
||||
small
|
||||
outlined
|
||||
>{{ t }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
|
@ -205,6 +254,12 @@ export default Vue.extend({
|
|||
return 15
|
||||
}
|
||||
},
|
||||
readingDirection (): string {
|
||||
return this.$_.capitalize(this.series.metadata.readingDirection.replace(/_/g, ' '))
|
||||
},
|
||||
languageDisplay (): string {
|
||||
return tags(this.series.metadata.language).language().descriptions()[0]
|
||||
},
|
||||
},
|
||||
props: {
|
||||
seriesId: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue