mirror of
https://github.com/gotson/komga.git
synced 2025-12-25 18:03:56 +01:00
1139 lines
46 KiB
Vue
1139 lines
46 KiB
Vue
<template>
|
|
<v-dialog v-model="modal"
|
|
:fullscreen="$vuetify.breakpoint.xsOnly"
|
|
max-width="800"
|
|
@keydown.esc="dialogCancel"
|
|
>
|
|
<form novalidate>
|
|
<v-card>
|
|
<v-toolbar class="hidden-sm-and-up">
|
|
<v-btn icon @click="dialogCancel">
|
|
<v-icon>mdi-close</v-icon>
|
|
</v-btn>
|
|
<v-toolbar-title>{{ dialogTitle }}</v-toolbar-title>
|
|
<v-spacer/>
|
|
<v-toolbar-items>
|
|
<v-btn text color="primary" @click="dialogConfirm">{{ $t('dialog.edit_books.button_confirm') }}</v-btn>
|
|
</v-toolbar-items>
|
|
</v-toolbar>
|
|
|
|
<v-card-title class="hidden-xs-only">
|
|
<v-icon class="mx-4">mdi-pencil</v-icon>
|
|
{{ dialogTitle }}
|
|
</v-card-title>
|
|
|
|
<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>
|
|
{{ $t('dialog.edit_books.tab_general') }}
|
|
</v-tab>
|
|
<v-tab class="justify-start">
|
|
<v-icon left class="hidden-xs-only">mdi-account-multiple</v-icon>
|
|
{{ $t('dialog.edit_books.tab_authors') }}
|
|
</v-tab>
|
|
<v-tab class="justify-start">
|
|
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
|
|
{{ $t('dialog.edit_books.tab_tags') }}
|
|
</v-tab>
|
|
<v-tab class="justify-start" v-if="single">
|
|
<v-icon left class="hidden-xs-only">mdi-link</v-icon>
|
|
{{ $t('dialog.edit_books.tab_links') }}
|
|
</v-tab>
|
|
<v-tab class="justify-start" v-if="single">
|
|
<v-icon left class="hidden-xs-only">mdi-image</v-icon>
|
|
{{ $t('dialog.edit_books.tab_poster') }}
|
|
</v-tab>
|
|
<v-tab class="justify-start">
|
|
<v-icon left class="hidden-xs-only">mdi-account-multiple</v-icon>
|
|
{{ $t('dialog.edit_series.tab_sharing') }}
|
|
</v-tab>
|
|
|
|
<!-- Tab: General -->
|
|
<v-tab-item>
|
|
<v-card flat>
|
|
<v-container fluid>
|
|
|
|
<!-- Title -->
|
|
<v-row v-if="single">
|
|
<v-col cols="12">
|
|
<v-text-field v-model="form.book.title"
|
|
:label="$t('dialog.edit_books.field_title')"
|
|
filled
|
|
dense
|
|
:error-messages="requiredBookErrors('title')"
|
|
@input="$v.form.book.title.$touch()"
|
|
@blur="$v.form.book.title.$touch()"
|
|
@change="form.book.titleLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.titleLock ? 'secondary' : ''"
|
|
@click="form.book.titleLock = !form.book.titleLock"
|
|
>
|
|
{{ form.book.titleLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Sort Title -->
|
|
<v-row v-if="single">
|
|
<v-col cols="12">
|
|
<v-text-field v-model="form.series.titleSort"
|
|
:label="$t('dialog.edit_series.field_sort_title')"
|
|
filled
|
|
dense
|
|
:error-messages="requiredSeriesErrors('titleSort')"
|
|
@input="$v.form.series.titleSort.$touch()"
|
|
@blur="$v.form.series.titleSort.$touch()"
|
|
@change="form.series.titleSortLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.titleSortLock ? 'secondary' : ''"
|
|
@click="form.series.titleSortLock = !form.series.titleSortLock"
|
|
>
|
|
{{ form.series.titleSortLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Summary -->
|
|
<v-row v-if="single">
|
|
<v-col cols="12">
|
|
<v-textarea v-model="form.book.summary"
|
|
:label="$t('dialog.edit_books.field_summary')"
|
|
filled
|
|
dense
|
|
@input="$v.form.book.summary.$touch()"
|
|
@change="form.book.summaryLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.summaryLock ? 'secondary' : ''"
|
|
@click="form.book.summaryLock = !form.book.summaryLock"
|
|
>
|
|
{{ form.book.summaryLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-textarea>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<!-- Reading Direction -->
|
|
<v-col cols="6">
|
|
<v-select v-model="form.series.readingDirection"
|
|
:items="readingDirections"
|
|
:label="$t('dialog.edit_series.field_reading_direction')"
|
|
clearable
|
|
filled
|
|
:placeholder="!single && mixed.readingDirection ? $t('dialog.edit_series.mixed') : ''"
|
|
@input="$v.form.series.readingDirection.$touch()"
|
|
@change="form.series.readingDirectionLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.readingDirectionLock ? 'secondary' : ''"
|
|
@click="form.series.readingDirectionLock = !form.series.readingDirectionLock"
|
|
>
|
|
{{ form.series.readingDirectionLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-select>
|
|
</v-col>
|
|
|
|
<!-- Language -->
|
|
<v-col cols="6">
|
|
<v-text-field v-model="form.series.language"
|
|
:label="$t('dialog.edit_series.field_language')"
|
|
filled
|
|
dense
|
|
:placeholder="!single && mixed.language ? $t('dialog.edit_series.mixed') : ''"
|
|
:error-messages="languageErrors"
|
|
:hint="$t('dialog.edit_series.field_language_hint')"
|
|
@input="$v.form.series.language.$touch()"
|
|
@change="form.series.languageLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.languageLock ? 'secondary' : ''"
|
|
@click="form.series.languageLock = !form.series.languageLock"
|
|
>
|
|
{{ form.series.languageLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row>
|
|
<!-- Publisher -->
|
|
<v-col cols="6">
|
|
<v-text-field v-model="form.series.publisher"
|
|
:label="$t('dialog.edit_series.field_publisher')"
|
|
filled
|
|
dense
|
|
:placeholder="!single && mixed.publisher ? $t('dialog.edit_series.mixed') : ''"
|
|
@input="$v.form.series.publisher.$touch()"
|
|
@change="form.series.publisherLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.publisherLock ? 'secondary' : ''"
|
|
@click="form.series.publisherLock = !form.series.publisherLock"
|
|
>
|
|
{{ form.series.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.series.ageRating"
|
|
:label="$t('dialog.edit_series.field_age_rating')"
|
|
clearable
|
|
filled
|
|
dense
|
|
type="number"
|
|
:placeholder="!single && mixed.ageRating ? $t('dialog.edit_series.mixed') : ''"
|
|
:error-messages="ageRatingErrors"
|
|
@input="$v.form.series.ageRating.$touch()"
|
|
@blur="$v.form.series.ageRating.$touch()"
|
|
@change="form.series.ageRatingLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.ageRatingLock ? 'secondary' : ''"
|
|
@click="form.series.ageRatingLock = !form.series.ageRatingLock"
|
|
>
|
|
{{ form.series.ageRatingLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<v-row v-if="single">
|
|
<!-- Release Date -->
|
|
<v-col cols="6">
|
|
<v-menu
|
|
v-model="menuDate"
|
|
:close-on-content-click="false"
|
|
transition="scale-transition"
|
|
offset-y
|
|
min-width="auto"
|
|
>
|
|
<template v-slot:activator="{on, attrs}">
|
|
<v-text-field v-model="form.book.releaseDate"
|
|
:label="$t('dialog.edit_books.field_release_date')"
|
|
filled
|
|
dense
|
|
placeholder="YYYY-MM-DD"
|
|
clearable
|
|
:error-messages="releaseDateErrors"
|
|
@blur="$v.form.book.releaseDate.$touch()"
|
|
@change="form.book.releaseDateLock = true"
|
|
v-bind="attrs"
|
|
v-on="on"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.releaseDateLock ? 'secondary' : ''"
|
|
@click="form.book.releaseDateLock = !form.book.releaseDateLock"
|
|
>
|
|
{{ form.book.releaseDateLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</template>
|
|
<v-date-picker v-model="form.book.releaseDate"
|
|
:show-current="true"
|
|
:show-adjacent-months="true"
|
|
:locale="$i18n.locale"
|
|
:first-day-of-week="$store.getters.getLocaleFirstDay()"
|
|
@input="menuDate = false"
|
|
@change="form.book.releaseDateLock = true"
|
|
/>
|
|
</v-menu>
|
|
</v-col>
|
|
|
|
<!-- ISBN -->
|
|
<v-col cols="6">
|
|
<v-text-field v-model.trim="form.book.isbn"
|
|
:label="$t('dialog.edit_books.field_isbn')"
|
|
filled
|
|
dense
|
|
placeholder="978-2-20-504375-4"
|
|
clearable
|
|
:error-messages="isbnErrors"
|
|
@blur="$v.form.book.isbn.$touch()"
|
|
@change="form.book.isbnLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.isbnLock ? 'secondary' : ''"
|
|
@click="form.book.isbnLock = !form.book.isbnLock"
|
|
>
|
|
{{ form.book.isbnLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
</v-container>
|
|
</v-card>
|
|
</v-tab-item>
|
|
|
|
<!-- Tab: Authors -->
|
|
<v-tab-item>
|
|
<v-card flat>
|
|
<v-container fluid>
|
|
<v-alert v-if="!single"
|
|
type="warning"
|
|
outlined
|
|
dense
|
|
>{{ $t('dialog.edit_books.authors_notice_multiple_edit') }}
|
|
</v-alert>
|
|
<v-row v-for="(role, i) in authorRoles"
|
|
:key="i"
|
|
>
|
|
<v-col cols="12">
|
|
<span class="text-body-2">{{ $_.capitalize(role.name) }}</span>
|
|
<v-combobox v-model="form.book.authors[role.value]"
|
|
:items="authorSearchResultsFull"
|
|
:search-input.sync="authorSearch[i]"
|
|
@keydown.esc="authorSearch[i] = null"
|
|
@input="$v.form.book.authors.$touch()"
|
|
@change="form.book.authorsLock = true"
|
|
hide-selected
|
|
chips
|
|
deletable-chips
|
|
multiple
|
|
filled
|
|
dense
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.authorsLock ? 'secondary' : ''"
|
|
@click="form.book.authorsLock = !form.book.authorsLock"
|
|
>
|
|
{{ form.book.authorsLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-combobox>
|
|
</v-col>
|
|
</v-row>
|
|
<v-row>
|
|
<v-col cols="12" md="6">
|
|
<v-form
|
|
v-model="customRoleValid"
|
|
ref="customRoleForm"
|
|
@submit.prevent="addRole"
|
|
>
|
|
<v-text-field
|
|
label="Add custom role"
|
|
append-outer-icon="mdi-plus"
|
|
v-model.trim="customRole"
|
|
@click:append-outer="addRole"
|
|
:rules="customRoleRules"
|
|
></v-text-field>
|
|
</v-form>
|
|
</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
|
|
>{{ $t('dialog.edit_books.tags_notice_multiple_edit') }}
|
|
</v-alert>
|
|
|
|
<!-- Genres -->
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<span class="text-body-2">{{ $t('dialog.edit_series.field_genres') }}</span>
|
|
<v-combobox v-model="form.series.genres"
|
|
:items="genresAvailable"
|
|
@input="$v.form.series.genres.$touch()"
|
|
@change="form.series.genresLock = true"
|
|
hide-selected
|
|
chips
|
|
deletable-chips
|
|
multiple
|
|
filled
|
|
dense
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.genresLock ? 'secondary' : ''"
|
|
@click="form.series.genresLock = !form.series.genresLock"
|
|
>
|
|
{{ form.series.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">{{ $t('dialog.edit_books.field_tags') }}</span>
|
|
<v-combobox v-model="form.book.tags"
|
|
:items="tagsAvailable"
|
|
@input="$v.form.book.tags.$touch()"
|
|
@change="form.book.tagsLock = true"
|
|
hide-selected
|
|
chips
|
|
deletable-chips
|
|
multiple
|
|
filled
|
|
dense
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.tagsLock ? 'secondary' : ''"
|
|
@click="form.book.tagsLock = !form.book.tagsLock"
|
|
>
|
|
{{ form.book.tagsLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-combobox>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
</v-card>
|
|
</v-tab-item>
|
|
|
|
<!-- Tab: Links -->
|
|
<v-tab-item v-if="single">
|
|
<v-card flat min-height="100">
|
|
<v-container fluid>
|
|
<!-- Links -->
|
|
<v-form
|
|
v-model="linksValid"
|
|
ref="linksForm"
|
|
>
|
|
<v-row
|
|
v-for="(link, i) in form.book.links"
|
|
:key="i"
|
|
>
|
|
<v-col cols="4" class="py-0">
|
|
<v-text-field v-model="form.book.links[i].label"
|
|
:label="$t('dialog.edit_books.field_link_label')"
|
|
filled
|
|
dense
|
|
:rules="[linksLabelRules]"
|
|
@input="$v.form.book.links.$touch()"
|
|
@blur="$v.form.book.links.$touch()"
|
|
@change="form.book.linksLock = true"
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.book.linksLock ? 'secondary' : ''"
|
|
@click="form.book.linksLock = !form.book.linksLock"
|
|
>
|
|
{{ form.book.linksLock ? 'mdi-lock' : 'mdi-lock-open' }}
|
|
</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
|
|
<v-col cols="8" class="py-0">
|
|
<v-text-field v-model="form.book.links[i].url"
|
|
:label="$t('dialog.edit_books.field_link_url')"
|
|
filled
|
|
dense
|
|
:rules="[linksUrlRules]"
|
|
@input="$v.form.book.links.$touch()"
|
|
@blur="$v.form.book.links.$touch()"
|
|
@change="form.book.linksLock = true"
|
|
>
|
|
<template v-slot:append-outer>
|
|
<v-icon @click="form.book.links.splice(i, 1)">mdi-delete</v-icon>
|
|
</template>
|
|
</v-text-field>
|
|
</v-col>
|
|
</v-row>
|
|
</v-form>
|
|
|
|
<v-row>
|
|
<v-spacer/>
|
|
<v-col cols="auto">
|
|
<v-btn
|
|
elevation="2"
|
|
fab
|
|
small
|
|
bottom
|
|
right
|
|
color="primary"
|
|
@click="form.book.links.push({label:'', url:''})"
|
|
>
|
|
<v-icon>mdi-plus</v-icon>
|
|
</v-btn>
|
|
</v-col>
|
|
</v-row>
|
|
</v-container>
|
|
</v-card>
|
|
</v-tab-item>
|
|
|
|
<!-- Tab: Thumbnails -->
|
|
<v-tab-item v-if="single">
|
|
<v-card flat>
|
|
<v-container fluid>
|
|
<!-- Upload -->
|
|
<v-row>
|
|
<v-col class="pa-1">
|
|
<drop-zone ref="thumbnailsUpload" @on-input-change="addThumbnail" class="pa-8"/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
<!-- Gallery -->
|
|
<v-row>
|
|
<v-col
|
|
cols="6" sm="4" lg="3" class="pa-1"
|
|
v-for="(item, index) in [...poster.uploadQueue, ...poster.bookThumbnails]"
|
|
:key="index"
|
|
>
|
|
<thumbnail-card
|
|
:item="item"
|
|
:selected="isThumbnailSelected(item)"
|
|
:toBeDeleted="isThumbnailToBeDeleted(item)"
|
|
@on-select-thumbnail="selectThumbnail"
|
|
@on-delete-thumbnail="deleteThumbnail"
|
|
/>
|
|
</v-col>
|
|
</v-row>
|
|
|
|
</v-container>
|
|
</v-card>
|
|
</v-tab-item>
|
|
|
|
<!-- Tab: Sharing -->
|
|
<v-tab-item>
|
|
<v-card flat>
|
|
<v-container fluid>
|
|
<v-alert v-if="!single"
|
|
type="warning"
|
|
outlined
|
|
dense
|
|
>{{ $t('dialog.edit_books.tags_notice_multiple_edit') }}
|
|
</v-alert>
|
|
|
|
<!-- Sharing Labels -->
|
|
<v-row>
|
|
<v-col cols="12">
|
|
<span class="text-body-2">{{ $t('dialog.edit_series.field_labels') }}</span>
|
|
<v-combobox v-model="form.series.sharingLabels"
|
|
:items="sharingLabelsAvailable"
|
|
@input="$v.form.series.sharingLabels.$touch()"
|
|
@change="form.series.sharingLabelsLock = true"
|
|
hide-selected
|
|
chips
|
|
deletable-chips
|
|
multiple
|
|
filled
|
|
dense
|
|
>
|
|
<template v-slot:prepend>
|
|
<v-icon :color="form.series.sharingLabelsLock ? 'secondary' : ''"
|
|
@click="form.series.sharingLabelsLock = !form.series.sharingLabelsLock"
|
|
>
|
|
{{ form.series.sharingLabelsLock ? '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">
|
|
<v-spacer/>
|
|
<v-btn text @click="dialogCancel">{{ $t('dialog.edit_books.button_cancel') }}</v-btn>
|
|
<v-btn color="primary" @click="dialogConfirm">{{ $t('dialog.edit_books.button_confirm') }}</v-btn>
|
|
</v-card-actions>
|
|
</v-card>
|
|
</form>
|
|
</v-dialog>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import Vue from 'vue'
|
|
import {helpers, minValue, requiredIf} from 'vuelidate/lib/validators'
|
|
import {ReadingDirection} from '@/types/enum-books'
|
|
import {Oneshot, SeriesDto} from '@/types/komga-series'
|
|
import {ERROR, ErrorEvent} from '@/types/events'
|
|
import DropZone from '@/components/DropZone.vue'
|
|
import ThumbnailCard from '@/components/ThumbnailCard.vue'
|
|
import {BookDto, BookThumbnailDto} from '@/types/komga-books'
|
|
import {isMatch} from 'date-fns'
|
|
import IsbnVerify from '@saekitominaga/isbn-verify'
|
|
import {debounce} from 'lodash'
|
|
import {authorRoles} from '@/types/author-roles'
|
|
import {buildManyAuthorsByRole, groupAuthorsByRole} from '@/functions/authors'
|
|
|
|
const tags = require('language-tags')
|
|
|
|
const validLanguage = (value: string) => !helpers.req(value) || tags.check(value)
|
|
const validDate = (value: string) => !helpers.req(value) || isMatch(value, 'yyyy-MM-dd')
|
|
const validIsbn = (value: string) => !helpers.req(value) || new IsbnVerify(value).isIsbn13({check_digit: true})
|
|
|
|
export default Vue.extend({
|
|
name: 'EditOneshotDialog',
|
|
components: {ThumbnailCard, DropZone},
|
|
data: () => {
|
|
return {
|
|
modal: false,
|
|
tab: 0,
|
|
menuDate: false,
|
|
customRole: '',
|
|
customRoles: [] as string[],
|
|
customRoleValid: false,
|
|
linksValid: false,
|
|
form: {
|
|
series: {
|
|
titleSort: '',
|
|
titleSortLock: false,
|
|
readingDirection: '',
|
|
readingDirectionLock: false,
|
|
publisher: '',
|
|
publisherLock: false,
|
|
ageRating: undefined as number | undefined,
|
|
ageRatingLock: false,
|
|
language: '',
|
|
languageLock: false,
|
|
genres: [],
|
|
genresLock: false,
|
|
sharingLabels: [],
|
|
sharingLabelsLock: false,
|
|
},
|
|
book: {
|
|
title: '',
|
|
titleLock: false,
|
|
summary: '',
|
|
summaryLock: false,
|
|
releaseDate: '',
|
|
releaseDateLock: false,
|
|
authors: {},
|
|
authorsLock: false,
|
|
tags: [],
|
|
tagsLock: false,
|
|
isbn: '',
|
|
isbnLock: false,
|
|
links: [],
|
|
linksLock: false,
|
|
},
|
|
},
|
|
mixed: {
|
|
readingDirection: false,
|
|
publisher: false,
|
|
ageRating: false,
|
|
language: false,
|
|
},
|
|
poster: {
|
|
selectedThumbnail: '',
|
|
uploadQueue: [] as File[],
|
|
deleteQueue: [] as BookThumbnailDto[],
|
|
bookThumbnails: [] as BookThumbnailDto[],
|
|
},
|
|
authorSearch: [],
|
|
authorSearchResults: [] as string[],
|
|
genresAvailable: [] as string[],
|
|
tagsAvailable: [] as string[],
|
|
sharingLabelsAvailable: [] as string[],
|
|
isMultiBookAuthorDirty: false, // workaround for author consistency in bulk mode
|
|
}
|
|
},
|
|
props: {
|
|
value: Boolean,
|
|
oneshots: {
|
|
type: [Object as () => Oneshot, Array as () => Oneshot[]],
|
|
required: true,
|
|
},
|
|
},
|
|
watch: {
|
|
value(val) {
|
|
this.modal = val
|
|
},
|
|
modal(val) {
|
|
if (val) {
|
|
this.getThumbnails(this.books)
|
|
this.loadAvailableTags()
|
|
this.loadAvailableGenres()
|
|
this.loadAvailableSharingLabels()
|
|
} else {
|
|
this.dialogCancel()
|
|
}
|
|
},
|
|
oneshots: {
|
|
immediate: true,
|
|
handler(val) {
|
|
this.dialogReset(val)
|
|
},
|
|
},
|
|
authorSearch: {
|
|
deep: true,
|
|
handler: debounce(async function (this: any, val: []) {
|
|
const index = val.findIndex(x => x !== null)
|
|
this.authorSearchResults = await this.$komgaReferential.getAuthorsNames(val[index])
|
|
}, 500),
|
|
},
|
|
},
|
|
validations: {
|
|
form: {
|
|
series: {
|
|
titleSort: {
|
|
required: requiredIf(function (this: any, model: any) {
|
|
return this.single
|
|
}),
|
|
},
|
|
language: {
|
|
validLanguage: validLanguage,
|
|
},
|
|
genres: {},
|
|
sharingLabels: {},
|
|
ageRating: {minValue: minValue(0)},
|
|
readingDirection: {},
|
|
publisher: {},
|
|
},
|
|
book: {
|
|
title: {
|
|
required: requiredIf(function (this: any, model: any) {
|
|
return this.single
|
|
}),
|
|
},
|
|
summary: {},
|
|
tags: {},
|
|
releaseDate: {validDate},
|
|
links: {},
|
|
authors: {},
|
|
isbn: {validIsbn},
|
|
},
|
|
},
|
|
},
|
|
computed: {
|
|
series(): SeriesDto | SeriesDto[] {
|
|
return this.single ? (this.oneshots as Oneshot).series : (this.oneshots as Oneshot[]).map(x => x.series)
|
|
},
|
|
books(): BookDto | BookDto[] {
|
|
return this.single ? (this.oneshots as Oneshot).book : (this.oneshots as Oneshot[]).map(x => x.book)
|
|
},
|
|
single(): boolean {
|
|
return !Array.isArray(this.oneshots)
|
|
},
|
|
authorRoles(): NameValue[] {
|
|
let remoteRoles = [] as string[]
|
|
if (Array.isArray(this.books))
|
|
remoteRoles = this.books.flatMap(b => b.metadata.authors).map(a => a.role)
|
|
else if (this.books?.metadata?.authors)
|
|
remoteRoles = this.books.metadata.authors.map(a => a.role)
|
|
const allRoles = this.$_.uniq([...authorRoles, ...remoteRoles, ...this.customRoles])
|
|
return allRoles.map((role: string) => ({
|
|
name: this.$te(`author_roles.${role}`) ? this.$t(`author_roles.${role}`).toString() : role,
|
|
value: role,
|
|
}))
|
|
},
|
|
authorSearchResultsFull(): string[] {
|
|
// merge local values with server search, so that already input value is available
|
|
const local = (this.$_.values(this.form.book.authors).flat()) as unknown as string[]
|
|
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
|
|
return this.$_.sortBy(this.$_.union(local, this.authorSearchResults), x => x.toLowerCase())
|
|
},
|
|
readingDirections(): any[] {
|
|
return Object.keys(ReadingDirection).map(x => (
|
|
{
|
|
text: this.$t(`enums.reading_direction.${x}`),
|
|
value: x,
|
|
}),
|
|
)
|
|
},
|
|
ageRatingErrors(): string[] {
|
|
const errors = [] as string[]
|
|
if (!this.$v.form?.series?.ageRating?.$dirty) return errors
|
|
!this.$v?.form?.series?.ageRating?.minValue && errors.push(this.$t('dialog.edit_series.field_age_rating_error').toString())
|
|
return errors
|
|
},
|
|
languageErrors(): string[] {
|
|
if (!this.$v.form?.series?.language?.$dirty) return []
|
|
if (!this.$v?.form?.series?.language?.validLanguage) return tags(this.form.series.language).errors().map((x: any) => x.message)
|
|
return []
|
|
},
|
|
dialogTitle(): string {
|
|
return this.single
|
|
? this.$t('dialog.edit_books.dialog_title_single', {book: this.$_.get(this.books, 'metadata.title')}).toString()
|
|
: this.$tc('dialog.edit_books.dialog_title_multiple', (this.books as BookDto[]).length)
|
|
},
|
|
releaseDateErrors(): string[] {
|
|
const errors = [] as string[]
|
|
if (!this.$v.form?.book?.releaseDate?.$dirty) return errors
|
|
!this.$v?.form?.book?.releaseDate?.validDate && errors.push(this.$t('dialog.edit_books.field_release_date_error').toString())
|
|
return errors
|
|
},
|
|
isbnErrors(): string[] {
|
|
const errors = [] as string[]
|
|
if (!this.$v.form?.book?.isbn?.$dirty) return errors
|
|
!this.$v?.form?.book?.isbn?.validIsbn && errors.push(this.$t('dialog.edit_books.field_isbn_error').toString())
|
|
return errors
|
|
},
|
|
customRoleRules(): any[] {
|
|
if (this.customRole === '') return [this.$t('common.required').toString()]
|
|
if (this.authorRoles.map(n => n.value.toLowerCase()).includes(this.customRole?.toLowerCase())) return [this.$t('dialog.edit_books.add_author_role_error_duplicate').toString()]
|
|
return [true]
|
|
},
|
|
},
|
|
methods: {
|
|
linksLabelRules(label: string): boolean | string {
|
|
if (!!this.$_.trim(label)) return true
|
|
return this.$t('common.required').toString()
|
|
},
|
|
linksUrlRules(value: string): boolean | string {
|
|
let url
|
|
try {
|
|
url = new URL(value)
|
|
} catch (_) {
|
|
return this.$t('dialog.edit_books.field_link_url_error_url').toString()
|
|
}
|
|
if (url.protocol === 'http:' || url.protocol === 'https:') return true
|
|
return this.$t('dialog.edit_books.field_link_url_error_protocol').toString()
|
|
},
|
|
addRole() {
|
|
if ((this.$refs.customRoleForm as any).validate()) {
|
|
this.customRoles.push(this.customRole.toLowerCase());
|
|
(this.$refs.customRoleForm as any).reset()
|
|
}
|
|
},
|
|
async loadAvailableTags() {
|
|
this.tagsAvailable = await this.$komgaReferential.getTags()
|
|
},
|
|
async loadAvailableGenres() {
|
|
this.genresAvailable = await this.$komgaReferential.getGenres()
|
|
},
|
|
async loadAvailableSharingLabels() {
|
|
this.sharingLabelsAvailable = await this.$komgaReferential.getSharingLabels()
|
|
},
|
|
requiredBookErrors(fieldName: string): string[] {
|
|
const errors = [] as string[]
|
|
const formField = this.$v.form.book!![fieldName] as any
|
|
if (!formField.$dirty) return errors
|
|
!formField.required && errors.push(this.$t('common.required').toString())
|
|
return errors
|
|
},
|
|
requiredSeriesErrors(fieldName: string): string[] {
|
|
const errors = [] as string[]
|
|
const formField = this.$v.form.series!![fieldName] as any
|
|
if (!formField.$dirty) return errors
|
|
!formField.required && errors.push(this.$t('common.required').toString())
|
|
return errors
|
|
},
|
|
dialogReset(oneshots: Oneshot | Oneshot[]) {
|
|
this.tab = 0;
|
|
(this.$refs.customRoleForm as any)?.reset();
|
|
(this.$refs.linksForm as any)?.resetValidation()
|
|
this.customRoles = []
|
|
this.$v.$reset()
|
|
if (Array.isArray(oneshots) && oneshots.length === 0) return
|
|
else if (this.$_.isEmpty(oneshots)) return
|
|
if (Array.isArray(oneshots) && oneshots.length > 0) {
|
|
const readingDirection = this.$_.uniq(oneshots.map(x => x.series.metadata.readingDirection))
|
|
this.form.series.readingDirection = readingDirection.length > 1 ? '' : readingDirection[0]
|
|
this.mixed.readingDirection = readingDirection.length > 1
|
|
|
|
const readingDirectionLock = this.$_.uniq(oneshots.map(x => x.series.metadata.readingDirectionLock))
|
|
this.form.series.readingDirectionLock = readingDirectionLock.length > 1 ? false : readingDirectionLock[0]
|
|
|
|
const ageRating = this.$_.uniq(oneshots.map(x => x.series.metadata.ageRating))
|
|
this.form.series.ageRating = ageRating.length > 1 ? undefined : ageRating[0]
|
|
this.mixed.ageRating = ageRating.length > 1
|
|
|
|
const ageRatingLock = this.$_.uniq(oneshots.map(x => x.series.metadata.ageRatingLock))
|
|
this.form.series.ageRatingLock = ageRatingLock.length > 1 ? false : ageRatingLock[0]
|
|
|
|
const publisher = this.$_.uniq(oneshots.map(x => x.series.metadata.publisher))
|
|
this.form.series.publisher = publisher.length > 1 ? '' : publisher[0]
|
|
this.mixed.publisher = publisher.length > 1
|
|
|
|
const publisherLock = this.$_.uniq(oneshots.map(x => x.series.metadata.publisherLock))
|
|
this.form.series.publisherLock = publisherLock.length > 1 ? false : publisherLock[0]
|
|
|
|
const language = this.$_.uniq(oneshots.map(x => x.series.metadata.language))
|
|
this.form.series.language = language.length > 1 ? '' : language[0]
|
|
this.mixed.language = language.length > 1
|
|
|
|
const languageLock = this.$_.uniq(oneshots.map(x => x.series.metadata.languageLock))
|
|
this.form.series.languageLock = languageLock.length > 1 ? false : languageLock[0]
|
|
|
|
this.form.series.genres = []
|
|
|
|
const genresLock = this.$_.uniq(oneshots.map(x => x.series.metadata.genresLock))
|
|
this.form.series.genresLock = genresLock.length > 1 ? false : genresLock[0]
|
|
|
|
this.form.book.tags = []
|
|
|
|
const tagsLock = this.$_.uniq(oneshots.map(x => x.book.metadata.tagsLock))
|
|
this.form.book.tagsLock = tagsLock.length > 1 ? false : tagsLock[0]
|
|
|
|
this.form.series.sharingLabels = []
|
|
|
|
const sharingLabelsLock = this.$_.uniq(oneshots.map(x => x.series.metadata.sharingLabelsLock))
|
|
this.form.series.sharingLabelsLock = sharingLabelsLock.length > 1 ? false : sharingLabelsLock[0]
|
|
|
|
this.form.book.links = []
|
|
this.form.book.authors = buildManyAuthorsByRole(oneshots.map(x => x.book))
|
|
|
|
let forceAuthorLock = false
|
|
for (const oneshot of oneshots) {
|
|
const bookAuthor = groupAuthorsByRole(oneshot.book.metadata.authors)
|
|
if (!this.$_.isEqual(bookAuthor, this.form.book.authors)) {
|
|
this.isMultiBookAuthorDirty = true
|
|
forceAuthorLock = true
|
|
break
|
|
}
|
|
}
|
|
|
|
const authorsLock = this.$_.uniq(oneshots.map(x => x.book.metadata.authorsLock))
|
|
this.form.book.authorsLock = authorsLock.length > 1 ? false : authorsLock[0] || forceAuthorLock
|
|
} else {
|
|
this.form.series.genres = []
|
|
this.form.series.sharingLabels = []
|
|
this.form.book.tags = []
|
|
this.form.book.links = []
|
|
const oneshot = oneshots as Oneshot
|
|
this.$_.merge(this.form.series, oneshot.series.metadata)
|
|
this.$_.merge(this.form.book, oneshot.book.metadata)
|
|
this.form.book.authors = groupAuthorsByRole(oneshot.book.metadata.authors)
|
|
this.poster.selectedThumbnail = ''
|
|
this.poster.deleteQueue = []
|
|
this.poster.uploadQueue = []
|
|
this.poster.bookThumbnails = []
|
|
}
|
|
},
|
|
dialogCancel() {
|
|
this.$emit('input', false)
|
|
this.dialogReset(this.oneshots)
|
|
},
|
|
async dialogConfirm() {
|
|
if (await this.editOneShots()) {
|
|
this.$emit('input', false)
|
|
}
|
|
},
|
|
validateForm(): any {
|
|
if (!this.$v.$invalid
|
|
&& (!this.single || !this.$refs.linksForm || (this.$refs.linksForm as any).validate())
|
|
) {
|
|
const metadataSeries = {
|
|
readingDirectionLock: this.form.series.readingDirectionLock,
|
|
ageRatingLock: this.form.series.ageRatingLock,
|
|
publisherLock: this.form.series.publisherLock,
|
|
languageLock: this.form.series.languageLock,
|
|
genresLock: this.form.series.genresLock,
|
|
sharingLabelsLock: this.form.series.sharingLabelsLock,
|
|
}
|
|
const metadataBook = {
|
|
authorsLock: this.form.book.authorsLock,
|
|
tagsLock: this.form.book.tagsLock,
|
|
}
|
|
|
|
if (this.$v.form?.book?.authors?.$dirty || this.isMultiBookAuthorDirty) {
|
|
this.$_.merge(metadataBook, {
|
|
authors: this.$_.keys(this.form.book.authors).flatMap((role: string) =>
|
|
this.$_.get(this.form.book.authors, role).map((name: string) => ({name: name, role: role})),
|
|
),
|
|
})
|
|
}
|
|
|
|
if (this.$v.form?.book?.tags?.$dirty) {
|
|
this.$_.merge(metadataBook, {tags: this.form.book.tags})
|
|
}
|
|
|
|
if (this.$v.form?.series?.readingDirection?.$dirty) {
|
|
this.$_.merge(metadataSeries, {readingDirection: this.form.series.readingDirection ? this.form.series.readingDirection : null})
|
|
}
|
|
|
|
if (this.$v.form?.series?.ageRating?.$dirty) {
|
|
this.$_.merge(metadataSeries, {ageRating: this.form.series.ageRating})
|
|
}
|
|
|
|
if (this.$v.form?.series?.publisher?.$dirty) {
|
|
this.$_.merge(metadataSeries, {publisher: this.form.series.publisher})
|
|
}
|
|
|
|
if (this.$v.form?.series?.genres?.$dirty) {
|
|
this.$_.merge(metadataSeries, {genres: this.form.series.genres})
|
|
}
|
|
|
|
if (this.$v.form?.series?.language?.$dirty) {
|
|
this.$_.merge(metadataSeries, {language: this.form.series.language})
|
|
}
|
|
|
|
if (this.$v.form?.series?.sharingLabels?.$dirty) {
|
|
this.$_.merge(metadataSeries, {sharingLabels: this.form.series.sharingLabels})
|
|
}
|
|
|
|
if (this.single) {
|
|
this.$_.merge(metadataSeries, {
|
|
titleLock: this.form.book.titleLock,
|
|
titleSortLock: this.form.series.titleSortLock,
|
|
summaryLock: this.form.book.summaryLock,
|
|
})
|
|
|
|
this.$_.merge(metadataBook, {
|
|
titleLock: this.form.book.titleLock,
|
|
summaryLock: this.form.book.summaryLock,
|
|
releaseDateLock: this.form.book.releaseDateLock,
|
|
isbnLock: this.form.book.isbnLock,
|
|
linksLock: this.form.book.linksLock,
|
|
})
|
|
|
|
if (this.$v.form?.series?.titleSort?.$dirty) {
|
|
this.$_.merge(metadataSeries, {titleSort: this.form.series.titleSort})
|
|
}
|
|
|
|
if (this.$v.form?.book?.title?.$dirty) {
|
|
this.$_.merge(metadataBook, {title: this.form.book.title})
|
|
this.$_.merge(metadataSeries, {title: this.form.book.title})
|
|
}
|
|
|
|
if (this.$v.form?.book?.summary?.$dirty) {
|
|
this.$_.merge(metadataBook, {summary: this.form.book.summary})
|
|
this.$_.merge(metadataSeries, {summary: this.form.book.summary})
|
|
}
|
|
|
|
if (this.$v.form?.book?.releaseDate?.$dirty) {
|
|
this.$_.merge(metadataBook, {releaseDate: this.form.book.releaseDate ? this.form.book.releaseDate : null})
|
|
}
|
|
|
|
if (this.$v.form?.book?.isbn?.$dirty) {
|
|
this.$_.merge(metadataBook, {isbn: this.form.book.isbn})
|
|
}
|
|
|
|
if (this.$v.form?.book?.links?.$dirty || this.form.book.links.length != (this.books as BookDto).metadata.links?.length) {
|
|
this.$_.merge(metadataBook, {links: this.form.book.links})
|
|
}
|
|
}
|
|
|
|
return {series: metadataSeries, book: metadataBook}
|
|
}
|
|
return null
|
|
},
|
|
async editOneShots(): Promise<boolean> {
|
|
if (this.single && this.poster.uploadQueue.length > 0) {
|
|
const book = this.books as BookDto
|
|
let hadErrors = false
|
|
for (const file of this.poster.uploadQueue.slice()) {
|
|
try {
|
|
await this.$komgaBooks.uploadThumbnail(book.id, file, file.name === this.poster.selectedThumbnail)
|
|
this.deleteThumbnail(file)
|
|
} catch (e) {
|
|
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
|
hadErrors = true
|
|
}
|
|
}
|
|
if (hadErrors) {
|
|
await this.getThumbnails(book)
|
|
return false
|
|
}
|
|
}
|
|
|
|
if (this.single && this.poster.selectedThumbnail !== '') {
|
|
const id = this.poster.selectedThumbnail
|
|
const book = this.books as BookDto
|
|
if (this.poster.bookThumbnails.find(value => value.id === id)) {
|
|
await this.$komgaBooks.markThumbnailAsSelected(book.id, id)
|
|
}
|
|
}
|
|
|
|
if (this.single && this.poster.deleteQueue.length > 0) {
|
|
this.poster.deleteQueue.forEach(toDelete => this.$komgaBooks.deleteThumbnail(toDelete.bookId, toDelete.id))
|
|
}
|
|
|
|
const metadata = this.validateForm()
|
|
if (metadata) {
|
|
const toUpdate = (this.single ? [this.oneshots] : this.oneshots) as Oneshot[]
|
|
for (const o of toUpdate) {
|
|
try {
|
|
await this.$komgaSeries.updateMetadata(o.series.id, metadata.series)
|
|
await this.$komgaBooks.updateMetadata(o.book.id, metadata.book)
|
|
} catch (e) {
|
|
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
|
}
|
|
}
|
|
return true
|
|
} else return false
|
|
},
|
|
addThumbnail(files: File[]) {
|
|
let hasSelected = false
|
|
for (const file of files) {
|
|
if (!this.poster.uploadQueue.find(value => value.name === file.name)) {
|
|
this.poster.uploadQueue.push(file)
|
|
if (!hasSelected) {
|
|
this.selectThumbnail(file)
|
|
hasSelected = true
|
|
}
|
|
}
|
|
}
|
|
|
|
(this.$refs.thumbnailsUpload as any).reset()
|
|
},
|
|
async getThumbnails(book: BookDto | BookDto[]) {
|
|
if (Array.isArray(book)) return
|
|
|
|
const thumbnails = await this.$komgaBooks.getThumbnails(book.id)
|
|
|
|
this.selectThumbnail(thumbnails.find(x => x.selected))
|
|
|
|
this.poster.bookThumbnails = thumbnails
|
|
},
|
|
isThumbnailSelected(item: File | BookThumbnailDto): boolean {
|
|
return item instanceof File ? item.name === this.poster.selectedThumbnail : item.id === this.poster.selectedThumbnail
|
|
},
|
|
selectThumbnail(item: File | BookThumbnailDto | undefined) {
|
|
if (!item) {
|
|
return
|
|
} else if (item instanceof File) {
|
|
this.poster.selectedThumbnail = item.name
|
|
} else {
|
|
const index = this.poster.deleteQueue.indexOf(item, 0)
|
|
if (index > -1) this.poster.deleteQueue.splice(index, 1)
|
|
|
|
this.poster.selectedThumbnail = item.id
|
|
}
|
|
},
|
|
isThumbnailToBeDeleted(item: File | BookThumbnailDto) {
|
|
if (item instanceof File) {
|
|
return false
|
|
} else {
|
|
return this.poster.deleteQueue.includes(item)
|
|
}
|
|
},
|
|
deleteThumbnail(item: File | BookThumbnailDto) {
|
|
if (item instanceof File) {
|
|
const index = this.poster.uploadQueue.indexOf(item, 0)
|
|
if (index > -1) {
|
|
this.poster.uploadQueue.splice(index, 1)
|
|
}
|
|
if (item.name === this.poster.selectedThumbnail) {
|
|
this.poster.selectedThumbnail = ''
|
|
}
|
|
} else {
|
|
// if thumbnail was marked for deletion, unmark it
|
|
if (this.isThumbnailToBeDeleted(item)) {
|
|
const index = this.poster.deleteQueue.indexOf(item, 0)
|
|
if (index > -1) {
|
|
this.poster.deleteQueue.splice(index, 1)
|
|
}
|
|
} else {
|
|
this.poster.deleteQueue.push(item)
|
|
if (item.id === this.poster.selectedThumbnail) this.poster.selectedThumbnail = ''
|
|
}
|
|
}
|
|
},
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<style lang="sass" scoped>
|
|
@import 'src/styles/tabbed-dialog'
|
|
</style>
|