diff --git a/komga-webui/src/components/dialogs/EditBooksDialog.vue b/komga-webui/src/components/dialogs/EditBooksDialog.vue index 647a32559..9bb5d0899 100644 --- a/komga-webui/src/components/dialogs/EditBooksDialog.vue +++ b/komga-webui/src/components/dialogs/EditBooksDialog.vue @@ -35,6 +35,10 @@ mdi-tag-multiple {{ $t('dialog.edit_books.tab_tags') }} + + mdi-link + {{ $t('dialog.edit_books.tab_links') }} + @@ -282,6 +286,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + mdi-plus + + + + + + + @@ -316,6 +391,7 @@ export default Vue.extend({ customRole: '', customRoles: [] as string[], customRoleValid: false, + linksValid: false, form: { title: '', titleLock: false, @@ -333,6 +409,8 @@ export default Vue.extend({ tagsLock: false, isbn: '', isbnLock: false, + links: [], + linksLock: false, }, authorSearch: [], authorSearchResults: [] as string[], @@ -389,6 +467,7 @@ export default Vue.extend({ summary: {}, authors: {}, isbn: {validIsbn}, + links: {}, }, }, async created() { @@ -434,12 +513,26 @@ export default Vue.extend({ return errors }, customRoleRules(): any[] { - if (this.customRole === '') return ['Must not be empty'] - if (this.authorRoles.map(n => n.value.toLowerCase()).includes(this.customRole?.toLowerCase())) return ['Already exists'] + 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()); @@ -456,12 +549,14 @@ export default Vue.extend({ dialogReset(books: BookDto | BookDto[]) { this.tab = 0; (this.$refs.customRoleForm as any)?.reset() + (this.$refs.linksForm as any)?.resetValidation() this.customRoles = [] this.$v.$reset() if (Array.isArray(books) && books.length === 0) return else if (this.$_.isEmpty(books)) return if (Array.isArray(books) && books.length > 0) { this.form.authors = {} + this.form.links = [] const authorsLock = this.$_.uniq(books.map(x => x.metadata.authorsLock)) 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] } else { this.form.tags = [] + this.form.links = [] const book = books as BookDto this.$_.merge(this.form, book.metadata) this.form.authors = groupAuthorsByRole(book.metadata.authors) @@ -487,7 +583,7 @@ export default Vue.extend({ } }, validateForm(): any { - if (!this.$v.$invalid) { + if (!this.$v.$invalid && (!this.single || (this.$refs.linksForm as any).validate())) { const metadata = { authorsLock: this.form.authorsLock, tagsLock: this.form.tagsLock, @@ -513,6 +609,7 @@ export default Vue.extend({ summaryLock: this.form.summaryLock, releaseDateLock: this.form.releaseDateLock, isbnLock: this.form.isbnLock, + linksLock: this.form.linksLock, }) if (this.$v.form?.title?.$dirty) { @@ -538,6 +635,10 @@ export default Vue.extend({ if (this.$v.form?.isbn?.$dirty) { 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 diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index eeedbfac0..2a72c4e04 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -322,6 +322,7 @@ "warning_html": "The user {name} will be deleted from this server. This cannot be undone. Continue?" }, "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.", "button_cancel": "Cancel", "button_confirm": "Save changes", @@ -329,6 +330,10 @@ "dialog_title_single": "Edit {book}", "field_isbn": "ISBN", "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_sort": "Sort Number", "field_number_sort_hint": "You can use decimal numbers", @@ -339,6 +344,7 @@ "field_title": "Title", "tab_authors": "Authors", "tab_general": "General", + "tab_links": "Links", "tab_tags": "Tags", "tags_notice_multiple_edit": "You are editing tags for multiple books. This will override existing tags of each book." }, diff --git a/komga-webui/src/types/komga-books.ts b/komga-webui/src/types/komga-books.ts index 86b7cf90a..0fbfb42a1 100644 --- a/komga-webui/src/types/komga-books.ts +++ b/komga-webui/src/types/komga-books.ts @@ -90,7 +90,9 @@ export interface BookMetadataUpdateDto { tags?: string[], tagsLock?: boolean isbn?: string, - isbnLock?: boolean + isbnLock?: boolean, + links?: WebLinkDto[], + linksLock?: boolean } export interface BookMetadataUpdateBatchDto { @@ -117,7 +119,7 @@ export interface BookFormat { color: string } -export interface BookImportBatchDto{ +export interface BookImportBatchDto { books: BookImportDto[], copyMode: CopyMode, } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/BookMetadataUpdateDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/BookMetadataUpdateDto.kt index c8dfbfbf0..cf60d06a4 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/BookMetadataUpdateDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/dto/BookMetadataUpdateDto.kt @@ -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.BookMetadata +import org.gotson.komga.domain.model.WebLink import org.gotson.komga.infrastructure.validation.NullOrBlankOrISBN import org.gotson.komga.infrastructure.validation.NullOrNotBlank +import org.hibernate.validator.constraints.URL +import java.net.URI import java.time.LocalDate import javax.validation.Valid import javax.validation.constraints.NotBlank @@ -63,6 +66,14 @@ class BookMetadataUpdateDto { } var isbnLock: Boolean? = null + + @get:Valid + var links: List? + by Delegates.observable(null) { prop, _, _ -> + isSet[prop.name] = true + } + + var linksLock: Boolean? = null } class AuthorUpdateDto { @@ -73,6 +84,14 @@ class AuthorUpdateDto { val role: String? = null } +class WebLinkUpdateDto { + @get:NotBlank + val label: String? = null + + @get:URL + val url: String? = null +} + fun BookMetadata.patch(patch: BookMetadataUpdateDto) = patch.let { this.copy( @@ -95,6 +114,10 @@ fun BookMetadata.patch(patch: BookMetadataUpdateDto) = } else this.tags, tagsLock = patch.tagsLock ?: this.tagsLock, 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, ) }