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 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ form.linksLock ? 'mdi-lock' : 'mdi-lock-open' }}
+
+
+
+
+
+
+
+
+ mdi-delete
+
+
+
+
+
+
+
+
+
+
+ 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,
)
}