mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
feat(webui): edit book links
This commit is contained in:
parent
394123d263
commit
719554766c
4 changed files with 138 additions and 6 deletions
|
|
@ -35,6 +35,10 @@
|
||||||
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
|
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
|
||||||
{{ $t('dialog.edit_books.tab_tags') }}
|
{{ $t('dialog.edit_books.tab_tags') }}
|
||||||
</v-tab>
|
</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>
|
||||||
|
|
||||||
<!-- Tab: General -->
|
<!-- Tab: General -->
|
||||||
<v-tab-item v-if="single">
|
<v-tab-item v-if="single">
|
||||||
|
|
@ -282,6 +286,77 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-tab-item>
|
</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.links"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<v-col cols="4" class="py-0">
|
||||||
|
<v-text-field v-model="form.links[i].label"
|
||||||
|
:label="$t('dialog.edit_books.field_link_label')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
:rules="[linksLabelRules]"
|
||||||
|
@input="$v.form.links.$touch()"
|
||||||
|
@blur="$v.form.links.$touch()"
|
||||||
|
@change="form.linksLock = true"
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon :color="form.linksLock ? 'secondary' : ''"
|
||||||
|
@click="form.linksLock = !form.linksLock"
|
||||||
|
>
|
||||||
|
{{ form.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.links[i].url"
|
||||||
|
:label="$t('dialog.edit_books.field_link_url')"
|
||||||
|
filled
|
||||||
|
dense
|
||||||
|
:rules="[linksUrlRules]"
|
||||||
|
@input="$v.form.links.$touch()"
|
||||||
|
@blur="$v.form.links.$touch()"
|
||||||
|
@change="form.linksLock = true"
|
||||||
|
>
|
||||||
|
<template v-slot:append-outer>
|
||||||
|
<v-icon @click="form.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.links.push({label:'', url:''})"
|
||||||
|
>
|
||||||
|
<v-icon>mdi-plus</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</v-card>
|
||||||
|
</v-tab-item>
|
||||||
|
|
||||||
</v-tabs>
|
</v-tabs>
|
||||||
|
|
||||||
<v-card-actions class="hidden-xs-only">
|
<v-card-actions class="hidden-xs-only">
|
||||||
|
|
@ -316,6 +391,7 @@ export default Vue.extend({
|
||||||
customRole: '',
|
customRole: '',
|
||||||
customRoles: [] as string[],
|
customRoles: [] as string[],
|
||||||
customRoleValid: false,
|
customRoleValid: false,
|
||||||
|
linksValid: false,
|
||||||
form: {
|
form: {
|
||||||
title: '',
|
title: '',
|
||||||
titleLock: false,
|
titleLock: false,
|
||||||
|
|
@ -333,6 +409,8 @@ export default Vue.extend({
|
||||||
tagsLock: false,
|
tagsLock: false,
|
||||||
isbn: '',
|
isbn: '',
|
||||||
isbnLock: false,
|
isbnLock: false,
|
||||||
|
links: [],
|
||||||
|
linksLock: false,
|
||||||
},
|
},
|
||||||
authorSearch: [],
|
authorSearch: [],
|
||||||
authorSearchResults: [] as string[],
|
authorSearchResults: [] as string[],
|
||||||
|
|
@ -389,6 +467,7 @@ export default Vue.extend({
|
||||||
summary: {},
|
summary: {},
|
||||||
authors: {},
|
authors: {},
|
||||||
isbn: {validIsbn},
|
isbn: {validIsbn},
|
||||||
|
links: {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
|
@ -434,12 +513,26 @@ export default Vue.extend({
|
||||||
return errors
|
return errors
|
||||||
},
|
},
|
||||||
customRoleRules(): any[] {
|
customRoleRules(): any[] {
|
||||||
if (this.customRole === '') return ['Must not be empty']
|
if (this.customRole === '') return [this.$t('common.required').toString()]
|
||||||
if (this.authorRoles.map(n => n.value.toLowerCase()).includes(this.customRole?.toLowerCase())) return ['Already exists']
|
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]
|
return [true]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
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() {
|
addRole() {
|
||||||
if ((this.$refs.customRoleForm as any).validate()) {
|
if ((this.$refs.customRoleForm as any).validate()) {
|
||||||
this.customRoles.push(this.customRole.toLowerCase());
|
this.customRoles.push(this.customRole.toLowerCase());
|
||||||
|
|
@ -456,12 +549,14 @@ export default Vue.extend({
|
||||||
dialogReset(books: BookDto | BookDto[]) {
|
dialogReset(books: BookDto | BookDto[]) {
|
||||||
this.tab = 0;
|
this.tab = 0;
|
||||||
(this.$refs.customRoleForm as any)?.reset()
|
(this.$refs.customRoleForm as any)?.reset()
|
||||||
|
(this.$refs.linksForm as any)?.resetValidation()
|
||||||
this.customRoles = []
|
this.customRoles = []
|
||||||
this.$v.$reset()
|
this.$v.$reset()
|
||||||
if (Array.isArray(books) && books.length === 0) return
|
if (Array.isArray(books) && books.length === 0) return
|
||||||
else if (this.$_.isEmpty(books)) return
|
else if (this.$_.isEmpty(books)) return
|
||||||
if (Array.isArray(books) && books.length > 0) {
|
if (Array.isArray(books) && books.length > 0) {
|
||||||
this.form.authors = {}
|
this.form.authors = {}
|
||||||
|
this.form.links = []
|
||||||
|
|
||||||
const authorsLock = this.$_.uniq(books.map(x => x.metadata.authorsLock))
|
const authorsLock = this.$_.uniq(books.map(x => x.metadata.authorsLock))
|
||||||
this.form.authorsLock = authorsLock.length > 1 ? false : authorsLock[0]
|
this.form.authorsLock = authorsLock.length > 1 ? false : authorsLock[0]
|
||||||
|
|
@ -472,6 +567,7 @@ export default Vue.extend({
|
||||||
this.form.tagsLock = tagsLock.length > 1 ? false : tagsLock[0]
|
this.form.tagsLock = tagsLock.length > 1 ? false : tagsLock[0]
|
||||||
} else {
|
} else {
|
||||||
this.form.tags = []
|
this.form.tags = []
|
||||||
|
this.form.links = []
|
||||||
const book = books as BookDto
|
const book = books as BookDto
|
||||||
this.$_.merge(this.form, book.metadata)
|
this.$_.merge(this.form, book.metadata)
|
||||||
this.form.authors = groupAuthorsByRole(book.metadata.authors)
|
this.form.authors = groupAuthorsByRole(book.metadata.authors)
|
||||||
|
|
@ -487,7 +583,7 @@ export default Vue.extend({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
validateForm(): any {
|
validateForm(): any {
|
||||||
if (!this.$v.$invalid) {
|
if (!this.$v.$invalid && (!this.single || (this.$refs.linksForm as any).validate())) {
|
||||||
const metadata = {
|
const metadata = {
|
||||||
authorsLock: this.form.authorsLock,
|
authorsLock: this.form.authorsLock,
|
||||||
tagsLock: this.form.tagsLock,
|
tagsLock: this.form.tagsLock,
|
||||||
|
|
@ -513,6 +609,7 @@ export default Vue.extend({
|
||||||
summaryLock: this.form.summaryLock,
|
summaryLock: this.form.summaryLock,
|
||||||
releaseDateLock: this.form.releaseDateLock,
|
releaseDateLock: this.form.releaseDateLock,
|
||||||
isbnLock: this.form.isbnLock,
|
isbnLock: this.form.isbnLock,
|
||||||
|
linksLock: this.form.linksLock,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.$v.form?.title?.$dirty) {
|
if (this.$v.form?.title?.$dirty) {
|
||||||
|
|
@ -538,6 +635,10 @@ export default Vue.extend({
|
||||||
if (this.$v.form?.isbn?.$dirty) {
|
if (this.$v.form?.isbn?.$dirty) {
|
||||||
this.$_.merge(metadata, {isbn: this.form.isbn})
|
this.$_.merge(metadata, {isbn: this.form.isbn})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.$v.form?.links?.$dirty || this.form.links.length != (this.books as BookDto).metadata.links?.length) {
|
||||||
|
this.$_.merge(metadata, {links: this.form.links})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
|
||||||
|
|
@ -322,6 +322,7 @@
|
||||||
"warning_html": "The user <b>{name}</b> will be deleted from this server. This <b>cannot</b> be undone. Continue?"
|
"warning_html": "The user <b>{name}</b> will be deleted from this server. This <b>cannot</b> be undone. Continue?"
|
||||||
},
|
},
|
||||||
"edit_books": {
|
"edit_books": {
|
||||||
|
"add_author_role_error_duplicate": "Already exists",
|
||||||
"authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.",
|
"authors_notice_multiple_edit": "You are editing authors for multiple books. This will override existing authors of each book.",
|
||||||
"button_cancel": "Cancel",
|
"button_cancel": "Cancel",
|
||||||
"button_confirm": "Save changes",
|
"button_confirm": "Save changes",
|
||||||
|
|
@ -329,6 +330,10 @@
|
||||||
"dialog_title_single": "Edit {book}",
|
"dialog_title_single": "Edit {book}",
|
||||||
"field_isbn": "ISBN",
|
"field_isbn": "ISBN",
|
||||||
"field_isbn_error": "Must be a valid ISBN 13",
|
"field_isbn_error": "Must be a valid ISBN 13",
|
||||||
|
"field_link_label": "Label",
|
||||||
|
"field_link_url": "URL",
|
||||||
|
"field_link_url_error_protocol": "Must be http or https",
|
||||||
|
"field_link_url_error_url": "Must be a valid URL",
|
||||||
"field_number": "Number",
|
"field_number": "Number",
|
||||||
"field_number_sort": "Sort Number",
|
"field_number_sort": "Sort Number",
|
||||||
"field_number_sort_hint": "You can use decimal numbers",
|
"field_number_sort_hint": "You can use decimal numbers",
|
||||||
|
|
@ -339,6 +344,7 @@
|
||||||
"field_title": "Title",
|
"field_title": "Title",
|
||||||
"tab_authors": "Authors",
|
"tab_authors": "Authors",
|
||||||
"tab_general": "General",
|
"tab_general": "General",
|
||||||
|
"tab_links": "Links",
|
||||||
"tab_tags": "Tags",
|
"tab_tags": "Tags",
|
||||||
"tags_notice_multiple_edit": "You are editing tags for multiple books. This will override existing tags of each book."
|
"tags_notice_multiple_edit": "You are editing tags for multiple books. This will override existing tags of each book."
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,9 @@ export interface BookMetadataUpdateDto {
|
||||||
tags?: string[],
|
tags?: string[],
|
||||||
tagsLock?: boolean
|
tagsLock?: boolean
|
||||||
isbn?: string,
|
isbn?: string,
|
||||||
isbnLock?: boolean
|
isbnLock?: boolean,
|
||||||
|
links?: WebLinkDto[],
|
||||||
|
linksLock?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookMetadataUpdateBatchDto {
|
export interface BookMetadataUpdateBatchDto {
|
||||||
|
|
@ -117,7 +119,7 @@ export interface BookFormat {
|
||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BookImportBatchDto{
|
export interface BookImportBatchDto {
|
||||||
books: BookImportDto[],
|
books: BookImportDto[],
|
||||||
copyMode: CopyMode,
|
copyMode: CopyMode,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ package org.gotson.komga.interfaces.api.rest.dto
|
||||||
|
|
||||||
import org.gotson.komga.domain.model.Author
|
import org.gotson.komga.domain.model.Author
|
||||||
import org.gotson.komga.domain.model.BookMetadata
|
import org.gotson.komga.domain.model.BookMetadata
|
||||||
|
import org.gotson.komga.domain.model.WebLink
|
||||||
import org.gotson.komga.infrastructure.validation.NullOrBlankOrISBN
|
import org.gotson.komga.infrastructure.validation.NullOrBlankOrISBN
|
||||||
import org.gotson.komga.infrastructure.validation.NullOrNotBlank
|
import org.gotson.komga.infrastructure.validation.NullOrNotBlank
|
||||||
|
import org.hibernate.validator.constraints.URL
|
||||||
|
import java.net.URI
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import javax.validation.Valid
|
import javax.validation.Valid
|
||||||
import javax.validation.constraints.NotBlank
|
import javax.validation.constraints.NotBlank
|
||||||
|
|
@ -63,6 +66,14 @@ class BookMetadataUpdateDto {
|
||||||
}
|
}
|
||||||
|
|
||||||
var isbnLock: Boolean? = null
|
var isbnLock: Boolean? = null
|
||||||
|
|
||||||
|
@get:Valid
|
||||||
|
var links: List<WebLinkUpdateDto>?
|
||||||
|
by Delegates.observable(null) { prop, _, _ ->
|
||||||
|
isSet[prop.name] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var linksLock: Boolean? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
class AuthorUpdateDto {
|
class AuthorUpdateDto {
|
||||||
|
|
@ -73,6 +84,14 @@ class AuthorUpdateDto {
|
||||||
val role: String? = null
|
val role: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class WebLinkUpdateDto {
|
||||||
|
@get:NotBlank
|
||||||
|
val label: String? = null
|
||||||
|
|
||||||
|
@get:URL
|
||||||
|
val url: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
fun BookMetadata.patch(patch: BookMetadataUpdateDto) =
|
fun BookMetadata.patch(patch: BookMetadataUpdateDto) =
|
||||||
patch.let {
|
patch.let {
|
||||||
this.copy(
|
this.copy(
|
||||||
|
|
@ -95,6 +114,10 @@ fun BookMetadata.patch(patch: BookMetadataUpdateDto) =
|
||||||
} else this.tags,
|
} else this.tags,
|
||||||
tagsLock = patch.tagsLock ?: this.tagsLock,
|
tagsLock = patch.tagsLock ?: this.tagsLock,
|
||||||
isbn = if (patch.isSet("isbn")) patch.isbn?.filter { it.isDigit() } ?: "" else this.isbn,
|
isbn = if (patch.isSet("isbn")) patch.isbn?.filter { it.isDigit() } ?: "" else this.isbn,
|
||||||
isbnLock = patch.isbnLock ?: this.isbnLock
|
isbnLock = patch.isbnLock ?: this.isbnLock,
|
||||||
|
links = if (patch.isSet("links")) {
|
||||||
|
if (patch.links != null) patch.links!!.map { WebLink(it.label!!, URI(it.url!!)) } else emptyList()
|
||||||
|
} else this.links,
|
||||||
|
linksLock = patch.linksLock ?: this.linksLock,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue