mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
parent
f9f02a395b
commit
8e0655f29a
17 changed files with 282 additions and 15 deletions
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
<template v-slot:more="value">
|
||||
<v-btn text small color="grey darken-1">
|
||||
{{ value.open ? $t('read_more.less') : $t('read_more.more') }}
|
||||
{{ value.open ? $t(i18nLess) : $t(i18nMore) }}
|
||||
<v-icon right>mdi-chevron-{{ value.open ? 'up' : 'down' }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
|
@ -20,6 +20,16 @@ import Vue from 'vue'
|
|||
export default Vue.extend({
|
||||
name: 'ReadMore',
|
||||
components: { VueReadMoreSmooth },
|
||||
props: {
|
||||
i18nMore: {
|
||||
type: String,
|
||||
default: 'read_more.more',
|
||||
},
|
||||
i18nLess: {
|
||||
type: String,
|
||||
default: 'read_more.less',
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,10 @@
|
|||
<v-icon left class="hidden-xs-only">mdi-format-align-center</v-icon>
|
||||
{{ $t('dialog.edit_series.tab_general') }}
|
||||
</v-tab>
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-format-title</v-icon>
|
||||
{{ $t('dialog.edit_series.tab_titles') }}
|
||||
</v-tab>
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-tag-multiple</v-icon>
|
||||
{{ $t('dialog.edit_series.tab_tags') }}
|
||||
|
|
@ -258,6 +262,77 @@
|
|||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Tab: Alternate Titles -->
|
||||
<v-tab-item v-if="single">
|
||||
<v-card flat min-height="100">
|
||||
<v-container fluid>
|
||||
<!-- Titles -->
|
||||
<v-form
|
||||
v-model="titlesValid"
|
||||
ref="titlesForm"
|
||||
>
|
||||
<v-row
|
||||
v-for="(title, i) in form.alternateTitles"
|
||||
:key="i"
|
||||
>
|
||||
<v-col cols="4" class="py-0">
|
||||
<v-text-field v-model="form.alternateTitles[i].label"
|
||||
:label="$t('dialog.edit_books.field_link_label')"
|
||||
filled
|
||||
dense
|
||||
:rules="[alternateTitleRules]"
|
||||
@input="$v.form.alternateTitles.$touch()"
|
||||
@blur="$v.form.alternateTitles.$touch()"
|
||||
@change="form.alternateTitlesLock = true"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="form.alternateTitlesLock ? 'secondary' : ''"
|
||||
@click="form.alternateTitlesLock = !form.alternateTitlesLock"
|
||||
>
|
||||
{{ 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.alternateTitles[i].title"
|
||||
:label="$t('dialog.edit_books.field_alternate_title')"
|
||||
filled
|
||||
dense
|
||||
:rules="[alternateTitleRules]"
|
||||
@input="$v.form.alternateTitles.$touch()"
|
||||
@blur="$v.form.alternateTitles.$touch()"
|
||||
@change="form.alternateTitlesLock = true"
|
||||
>
|
||||
<template v-slot:append-outer>
|
||||
<v-icon @click="form.alternateTitles.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.alternateTitles.push({label:'', title:''})"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Tab: Tags -->
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
|
|
@ -500,6 +575,7 @@ export default Vue.extend({
|
|||
modal: false,
|
||||
tab: 0,
|
||||
linksValid: false,
|
||||
titlesValid: false,
|
||||
form: {
|
||||
status: '',
|
||||
statusLock: false,
|
||||
|
|
@ -527,6 +603,8 @@ export default Vue.extend({
|
|||
sharingLabelsLock: false,
|
||||
links: [],
|
||||
linksLock: false,
|
||||
alternateTitles: [],
|
||||
alternateTitlesLock: false,
|
||||
},
|
||||
mixed: {
|
||||
status: false,
|
||||
|
|
@ -601,6 +679,7 @@ export default Vue.extend({
|
|||
publisher: {},
|
||||
totalBookCount: {minValue: minValue(1)},
|
||||
links: {},
|
||||
alternateTitles: {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -645,6 +724,10 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
alternateTitleRules(text: string): boolean | string {
|
||||
if (!!this.$_.trim(text)) return true
|
||||
return this.$t('common.required').toString()
|
||||
},
|
||||
linksLabelRules(label: string): boolean | string {
|
||||
if (!!this.$_.trim(label)) return true
|
||||
return this.$t('common.required').toString()
|
||||
|
|
@ -678,7 +761,8 @@ export default Vue.extend({
|
|||
dialogReset(series: SeriesDto | SeriesDto[]) {
|
||||
this.tab = 0
|
||||
this.$v.$reset();
|
||||
(this.$refs.linksForm as any)?.resetValidation()
|
||||
(this.$refs.linksForm as any)?.resetValidation();
|
||||
(this.$refs.titlesForm as any)?.resetValidation()
|
||||
if (Array.isArray(series) && series.length === 0) return
|
||||
if (Array.isArray(series) && series.length > 0) {
|
||||
const status = this.$_.uniq(series.map(x => x.metadata.status))
|
||||
|
|
@ -732,11 +816,13 @@ export default Vue.extend({
|
|||
this.form.sharingLabelsLock = sharingLabelsLock.length > 1 ? false : sharingLabelsLock[0]
|
||||
|
||||
this.form.links = []
|
||||
this.form.alternateTitles = []
|
||||
} else {
|
||||
this.form.genres = []
|
||||
this.form.tags = []
|
||||
this.form.sharingLabels = []
|
||||
this.form.links = []
|
||||
this.form.alternateTitles = []
|
||||
this.$_.merge(this.form, (series as SeriesDto).metadata)
|
||||
this.poster.selectedThumbnail = ''
|
||||
this.poster.deleteQueue = []
|
||||
|
|
@ -754,7 +840,10 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
validateForm(): any {
|
||||
if (!this.$v.$invalid && (!this.single || !this.$refs.linksForm || (this.$refs.linksForm as any).validate())) {
|
||||
if (!this.$v.$invalid
|
||||
&& (!this.single || !this.$refs.linksForm || (this.$refs.linksForm as any).validate())
|
||||
&& (!this.single || !this.$refs.titlesForm || (this.$refs.titlesForm as any).validate())
|
||||
) {
|
||||
const metadata = {
|
||||
statusLock: this.form.statusLock,
|
||||
readingDirectionLock: this.form.readingDirectionLock,
|
||||
|
|
@ -766,6 +855,7 @@ export default Vue.extend({
|
|||
totalBookCountLock: this.form.totalBookCountLock,
|
||||
sharingLabelsLock: this.form.sharingLabelsLock,
|
||||
linksLock: this.form.linksLock,
|
||||
alternateTitlesLock: this.form.alternateTitlesLock,
|
||||
}
|
||||
|
||||
if (this.$v.form?.status?.$dirty) {
|
||||
|
|
@ -827,6 +917,10 @@ export default Vue.extend({
|
|||
if (this.$v.form?.links?.$dirty || this.form.links.length != (this.series as SeriesDto).metadata.links?.length) {
|
||||
this.$_.merge(metadata, {links: this.form.links})
|
||||
}
|
||||
|
||||
if (this.$v.form?.alternateTitles?.$dirty || this.form.alternateTitles.length != (this.series as SeriesDto).metadata.alternateTitles?.length) {
|
||||
this.$_.merge(metadata, {alternateTitles: this.form.alternateTitles})
|
||||
}
|
||||
}
|
||||
|
||||
return metadata
|
||||
|
|
|
|||
|
|
@ -347,6 +347,7 @@
|
|||
"button_confirm": "Save changes",
|
||||
"dialog_title_multiple": "Edit {count} book | Edit {count} books",
|
||||
"dialog_title_single": "Edit {book}",
|
||||
"field_alternate_title": "Alternate title",
|
||||
"field_isbn": "ISBN",
|
||||
"field_isbn_error": "Must be a valid ISBN 13",
|
||||
"field_link_label": "Label",
|
||||
|
|
@ -457,6 +458,7 @@
|
|||
"tab_poster": "Poster",
|
||||
"tab_sharing": "Sharing",
|
||||
"tab_tags": "Tags",
|
||||
"tab_titles": "Alternate Titles",
|
||||
"tags_notice_multiple_edit": "You are editing tags for multiple series. This will override existing tags of each series."
|
||||
},
|
||||
"edit_user": {
|
||||
|
|
@ -812,6 +814,10 @@
|
|||
"tooltip_too_big": "File too big!",
|
||||
"tooltip_user_uploaded": "User uploaded"
|
||||
},
|
||||
"titles_more": {
|
||||
"less": "Less titles",
|
||||
"more": "More titles"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Administrator",
|
||||
"FILE_DOWNLOAD": "File download",
|
||||
|
|
|
|||
|
|
@ -47,8 +47,10 @@ export interface SeriesMetadataDto {
|
|||
totalBookCountLock: boolean,
|
||||
sharingLabels: string[],
|
||||
sharingLabelsLock: boolean,
|
||||
links?: WebLinkDto[],
|
||||
linksLock?: boolean
|
||||
links: WebLinkDto[],
|
||||
linksLock: boolean,
|
||||
alternateTitles: AlternateTitleDto[],
|
||||
alternateTitlesLock: boolean,
|
||||
}
|
||||
|
||||
export interface SeriesBooksMetadataDto {
|
||||
|
|
@ -88,6 +90,8 @@ export interface SeriesMetadataUpdateDto {
|
|||
sharingLabelsLock: boolean,
|
||||
links?: WebLinkDto[],
|
||||
linksLock?: boolean,
|
||||
alternateTitles?: AlternateTitleDto[],
|
||||
alternateTitlesLock?: boolean,
|
||||
}
|
||||
|
||||
export interface GroupCountDto {
|
||||
|
|
@ -101,3 +105,8 @@ export interface SeriesThumbnailDto {
|
|||
type: string,
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
export interface AlternateTitleDto {
|
||||
label: string,
|
||||
title: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,7 +113,10 @@
|
|||
<v-tooltip right>
|
||||
<template v-slot:activator="{ on }">
|
||||
<span v-on="on">{{
|
||||
new Intl.DateTimeFormat($i18n.locale, {year: 'numeric', timeZone: 'UTC'}).format(new Date(series.booksMetadata.releaseDate))
|
||||
new Intl.DateTimeFormat($i18n.locale, {
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC'
|
||||
}).format(new Date(series.booksMetadata.releaseDate))
|
||||
}}</span>
|
||||
</template>
|
||||
{{ $t('browse_series.earliest_year_from_release_dates') }}
|
||||
|
|
@ -166,6 +169,17 @@
|
|||
</v-row>
|
||||
|
||||
<template v-if="$vuetify.breakpoint.smAndUp">
|
||||
<!-- Alternate titles -->
|
||||
<read-more class="mb-4" i18n-less="titles_more.less" i18n-more="titles_more.more">
|
||||
<v-row v-for="(a, i) in series.metadata.alternateTitles"
|
||||
:key="i"
|
||||
class="align-center text-caption"
|
||||
>
|
||||
<v-col cols="4" sm="3" md="2" xl="1" class="py-0 text-uppercase" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.label }}</v-col>
|
||||
<v-col cols="8" sm="9" md="10" xl="11" class="py-0" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.title }}</v-col>
|
||||
</v-row>
|
||||
</read-more>
|
||||
|
||||
<v-row class="align-center">
|
||||
<v-col cols="auto">
|
||||
<v-btn :title="$t('menu.download_series')"
|
||||
|
|
@ -203,6 +217,17 @@
|
|||
</v-row>
|
||||
|
||||
<template v-if="$vuetify.breakpoint.xsOnly">
|
||||
<!-- Alternate titles -->
|
||||
<read-more class="mb-4" i18n-less="titles_more.less" i18n-more="titles_more.more">
|
||||
<v-row v-for="(a, i) in series.metadata.alternateTitles"
|
||||
:key="i"
|
||||
class="align-center text-caption"
|
||||
>
|
||||
<v-col cols="4" class="py-0 text-uppercase" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.label }}</v-col>
|
||||
<v-col cols="8" class="py-0" :class="i===0 ? 'pt-4' : i === series.metadata.alternateTitles.length - 1 ? 'pb-4' : ''">{{ a.title }}</v-col>
|
||||
</v-row>
|
||||
</read-more>
|
||||
|
||||
<!-- Download button -->
|
||||
<v-row class="align-center">
|
||||
<v-col cols="auto">
|
||||
|
|
@ -529,9 +554,9 @@ export default Vue.extend({
|
|||
},
|
||||
computed: {
|
||||
itemContext(): ItemContext[] {
|
||||
if(this.sortActive.key === 'metadata.releaseDate') return [ItemContext.RELEASE_DATE]
|
||||
if(this.sortActive.key === 'createdDate') return [ItemContext.DATE_ADDED]
|
||||
if(this.sortActive.key === 'fileSize') return [ItemContext.FILE_SIZE]
|
||||
if (this.sortActive.key === 'metadata.releaseDate') return [ItemContext.RELEASE_DATE]
|
||||
if (this.sortActive.key === 'createdDate') return [ItemContext.DATE_ADDED]
|
||||
if (this.sortActive.key === 'fileSize') return [ItemContext.FILE_SIZE]
|
||||
return []
|
||||
},
|
||||
sortOptions(): SortOption[] {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
CREATE TABLE SERIES_METADATA_ALTERNATE_TITLE
|
||||
(
|
||||
LABEL varchar NOT NULL,
|
||||
TITLE varchar NOT NULL,
|
||||
SERIES_ID varchar NOT NULL,
|
||||
FOREIGN KEY (SERIES_ID) REFERENCES SERIES (ID)
|
||||
);
|
||||
|
||||
alter table series_metadata
|
||||
add column ALTERNATE_TITLES_LOCK boolean NOT NULL DEFAULT 0;
|
||||
|
||||
create index idx__series_metadata_alternate_title__series_id
|
||||
on SERIES_METADATA_ALTERNATE_TITLE (SERIES_ID);
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
data class AlternateTitle(
|
||||
val label: String,
|
||||
val title: String,
|
||||
)
|
||||
|
|
@ -17,6 +17,7 @@ class SeriesMetadata(
|
|||
val totalBookCount: Int? = null,
|
||||
sharingLabels: Set<String> = emptySet(),
|
||||
val links: List<WebLink> = emptyList(),
|
||||
val alternateTitles: List<AlternateTitle> = emptyList(),
|
||||
|
||||
val statusLock: Boolean = false,
|
||||
val titleLock: Boolean = false,
|
||||
|
|
@ -31,6 +32,7 @@ class SeriesMetadata(
|
|||
val totalBookCountLock: Boolean = false,
|
||||
val sharingLabelsLock: Boolean = false,
|
||||
val linksLock: Boolean = false,
|
||||
val alternateTitlesLock: Boolean = false,
|
||||
|
||||
val seriesId: String = "",
|
||||
|
||||
|
|
@ -60,6 +62,7 @@ class SeriesMetadata(
|
|||
totalBookCount: Int? = this.totalBookCount,
|
||||
sharingLabels: Set<String> = this.sharingLabels,
|
||||
links: List<WebLink> = this.links,
|
||||
alternateTitles: List<AlternateTitle> = this.alternateTitles,
|
||||
statusLock: Boolean = this.statusLock,
|
||||
titleLock: Boolean = this.titleLock,
|
||||
titleSortLock: Boolean = this.titleSortLock,
|
||||
|
|
@ -73,6 +76,7 @@ class SeriesMetadata(
|
|||
totalBookCountLock: Boolean = this.totalBookCountLock,
|
||||
sharingLabelsLock: Boolean = this.sharingLabelsLock,
|
||||
linksLock: Boolean = this.linksLock,
|
||||
alternateTitlesLock: Boolean = this.alternateTitlesLock,
|
||||
seriesId: String = this.seriesId,
|
||||
createdDate: LocalDateTime = this.createdDate,
|
||||
lastModifiedDate: LocalDateTime = this.lastModifiedDate,
|
||||
|
|
@ -91,6 +95,7 @@ class SeriesMetadata(
|
|||
totalBookCount = totalBookCount,
|
||||
sharingLabels = sharingLabels,
|
||||
links = links,
|
||||
alternateTitles = alternateTitles,
|
||||
statusLock = statusLock,
|
||||
titleLock = titleLock,
|
||||
titleSortLock = titleSortLock,
|
||||
|
|
@ -104,6 +109,7 @@ class SeriesMetadata(
|
|||
totalBookCountLock = totalBookCountLock,
|
||||
sharingLabelsLock = sharingLabelsLock,
|
||||
linksLock = linksLock,
|
||||
alternateTitlesLock = alternateTitlesLock,
|
||||
seriesId = seriesId,
|
||||
createdDate = createdDate,
|
||||
lastModifiedDate = lastModifiedDate,
|
||||
|
|
@ -121,5 +127,5 @@ class SeriesMetadata(
|
|||
}
|
||||
|
||||
override fun toString(): String =
|
||||
"SeriesMetadata(status=$status, readingDirection=$readingDirection, ageRating=$ageRating, totalBookCount=$totalBookCount, links=$links, statusLock=$statusLock, titleLock=$titleLock, titleSortLock=$titleSortLock, summaryLock=$summaryLock, readingDirectionLock=$readingDirectionLock, publisherLock=$publisherLock, ageRatingLock=$ageRatingLock, languageLock=$languageLock, genresLock=$genresLock, tagsLock=$tagsLock, totalBookCountLock=$totalBookCountLock, sharingLabelsLock=$sharingLabelsLock, linksLock=$linksLock, seriesId='$seriesId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', titleSort='$titleSort', summary='$summary', publisher='$publisher', language='$language', tags=$tags, genres=$genres, sharingLabels=$sharingLabels)"
|
||||
"SeriesMetadata(status=$status, readingDirection=$readingDirection, ageRating=$ageRating, totalBookCount=$totalBookCount, links=$links, alternateTitles=$alternateTitles, statusLock=$statusLock, titleLock=$titleLock, titleSortLock=$titleSortLock, summaryLock=$summaryLock, readingDirectionLock=$readingDirectionLock, publisherLock=$publisherLock, ageRatingLock=$ageRatingLock, languageLock=$languageLock, genresLock=$genresLock, tagsLock=$tagsLock, totalBookCountLock=$totalBookCountLock, sharingLabelsLock=$sharingLabelsLock, linksLock=$linksLock, alternateTitlesLock=$alternateTitlesLock, seriesId='$seriesId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate, title='$title', titleSort='$titleSort', summary='$summary', publisher='$publisher', language='$language', tags=$tags, genres=$genres, sharingLabels=$sharingLabels)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.gotson.komga.infrastructure.search.LuceneEntity
|
|||
import org.gotson.komga.infrastructure.search.LuceneHelper
|
||||
import org.gotson.komga.infrastructure.web.toFilePath
|
||||
import org.gotson.komga.interfaces.api.persistence.SeriesDtoRepository
|
||||
import org.gotson.komga.interfaces.api.rest.dto.AlternateTitleDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.AuthorDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.BookMetadataAggregationDto
|
||||
import org.gotson.komga.interfaces.api.rest.dto.GroupCountDto
|
||||
|
|
@ -62,6 +63,7 @@ class SeriesDtoDao(
|
|||
private val st = Tables.SERIES_METADATA_TAG
|
||||
private val sl = Tables.SERIES_METADATA_SHARING
|
||||
private val slk = Tables.SERIES_METADATA_LINK
|
||||
private val sat = Tables.SERIES_METADATA_ALTERNATE_TITLE
|
||||
private val bma = Tables.BOOK_METADATA_AGGREGATION
|
||||
private val bmaa = Tables.BOOK_METADATA_AGGREGATION_AUTHOR
|
||||
private val bmat = Tables.BOOK_METADATA_AGGREGATION_TAG
|
||||
|
|
@ -230,6 +232,7 @@ class SeriesDtoDao(
|
|||
lateinit var tags: Map<String, List<String>>
|
||||
lateinit var sharingLabels: Map<String, List<String>>
|
||||
lateinit var links: Map<String, List<WebLinkDto>>
|
||||
lateinit var alternateTitles: Map<String, List<AlternateTitleDto>>
|
||||
lateinit var aggregatedAuthors: Map<String, List<AuthorDto>>
|
||||
lateinit var aggregatedTags: Map<String, List<String>>
|
||||
transactionTemplate.executeWithoutResult {
|
||||
|
|
@ -250,6 +253,10 @@ class SeriesDtoDao(
|
|||
.where(slk.SERIES_ID.`in`(dsl.selectTempStrings()))
|
||||
.groupBy({ it.seriesId }, { WebLinkDto(it.label, it.url) })
|
||||
|
||||
alternateTitles = dsl.selectFrom(sat)
|
||||
.where(sat.SERIES_ID.`in`(dsl.selectTempStrings()))
|
||||
.groupBy({ it.seriesId }, { AlternateTitleDto(it.label, it.title) })
|
||||
|
||||
aggregatedAuthors = dsl.selectFrom(bmaa)
|
||||
.where(bmaa.SERIES_ID.`in`(dsl.selectTempStrings()))
|
||||
.filter { it.name != null }
|
||||
|
|
@ -275,7 +282,7 @@ class SeriesDtoDao(
|
|||
booksReadCount,
|
||||
booksUnreadCount,
|
||||
booksInProgressCount,
|
||||
dr.toDto(genres[sr.id].orEmpty().toSet(), tags[sr.id].orEmpty().toSet(), sharingLabels[sr.id].orEmpty().toSet(), links[sr.id].orEmpty()),
|
||||
dr.toDto(genres[sr.id].orEmpty().toSet(), tags[sr.id].orEmpty().toSet(), sharingLabels[sr.id].orEmpty().toSet(), links[sr.id].orEmpty(), alternateTitles[sr.id].orEmpty()),
|
||||
bmar.toDto(aggregatedAuthors[sr.id].orEmpty(), aggregatedTags[sr.id].orEmpty().toSet()),
|
||||
)
|
||||
}
|
||||
|
|
@ -372,7 +379,7 @@ class SeriesDtoDao(
|
|||
deleted = deletedDate != null,
|
||||
)
|
||||
|
||||
private fun SeriesMetadataRecord.toDto(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLinkDto>) =
|
||||
private fun SeriesMetadataRecord.toDto(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLinkDto>, alternateTitles: List<AlternateTitleDto>) =
|
||||
SeriesMetadataDto(
|
||||
status = status,
|
||||
statusLock = statusLock,
|
||||
|
|
@ -402,6 +409,8 @@ class SeriesDtoDao(
|
|||
sharingLabelsLock = sharingLabelsLock,
|
||||
links = links,
|
||||
linksLock = linksLock,
|
||||
alternateTitles = alternateTitles,
|
||||
alternateTitlesLock = alternateTitlesLock,
|
||||
)
|
||||
|
||||
private fun BookMetadataAggregationRecord.toDto(authors: List<AuthorDto>, tags: Set<String>) =
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package org.gotson.komga.infrastructure.jooq
|
||||
|
||||
import org.gotson.komga.domain.model.AlternateTitle
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.WebLink
|
||||
import org.gotson.komga.domain.persistence.SeriesMetadataRepository
|
||||
|
|
@ -24,12 +25,13 @@ class SeriesMetadataDao(
|
|||
private val st = Tables.SERIES_METADATA_TAG
|
||||
private val sl = Tables.SERIES_METADATA_SHARING
|
||||
private val slk = Tables.SERIES_METADATA_LINK
|
||||
private val sat = Tables.SERIES_METADATA_ALTERNATE_TITLE
|
||||
|
||||
override fun findById(seriesId: String): SeriesMetadata =
|
||||
findOne(seriesId)!!.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId))
|
||||
findOne(seriesId)!!.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId), findAlternateTitles(seriesId))
|
||||
|
||||
override fun findByIdOrNull(seriesId: String): SeriesMetadata? =
|
||||
findOne(seriesId)?.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId))
|
||||
findOne(seriesId)?.toDomain(findGenres(seriesId), findTags(seriesId), findSharingLabels(seriesId), findLinks(seriesId), findAlternateTitles(seriesId))
|
||||
|
||||
private fun findOne(seriesId: String) =
|
||||
dsl.selectFrom(d)
|
||||
|
|
@ -53,6 +55,7 @@ class SeriesMetadataDao(
|
|||
.from(sl)
|
||||
.where(sl.SERIES_ID.eq(seriesId))
|
||||
.fetchSet(sl.LABEL)
|
||||
|
||||
private fun findLinks(seriesId: String) =
|
||||
dsl.select(slk.LABEL, slk.URL)
|
||||
.from(slk)
|
||||
|
|
@ -60,6 +63,13 @@ class SeriesMetadataDao(
|
|||
.fetchInto(slk)
|
||||
.map { WebLink(it.label, URI(it.url)) }
|
||||
|
||||
private fun findAlternateTitles(seriesId: String) =
|
||||
dsl.select(sat.LABEL, sat.TITLE)
|
||||
.from(sat)
|
||||
.where(sat.SERIES_ID.eq(seriesId))
|
||||
.fetchInto(sat)
|
||||
.map { AlternateTitle(it.label, it.title) }
|
||||
|
||||
@Transactional
|
||||
override fun insert(metadata: SeriesMetadata) {
|
||||
dsl.insertInto(d)
|
||||
|
|
@ -86,12 +96,14 @@ class SeriesMetadataDao(
|
|||
.set(d.TOTAL_BOOK_COUNT_LOCK, metadata.totalBookCountLock)
|
||||
.set(d.SHARING_LABELS_LOCK, metadata.sharingLabelsLock)
|
||||
.set(d.LINKS_LOCK, metadata.linksLock)
|
||||
.set(d.ALTERNATE_TITLES_LOCK, metadata.alternateTitlesLock)
|
||||
.execute()
|
||||
|
||||
insertGenres(metadata)
|
||||
insertTags(metadata)
|
||||
insertSharingLabels(metadata)
|
||||
insertLinks(metadata)
|
||||
insertAlternateTitles(metadata)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
@ -119,6 +131,7 @@ class SeriesMetadataDao(
|
|||
.set(d.TOTAL_BOOK_COUNT_LOCK, metadata.totalBookCountLock)
|
||||
.set(d.SHARING_LABELS_LOCK, metadata.sharingLabelsLock)
|
||||
.set(d.LINKS_LOCK, metadata.linksLock)
|
||||
.set(d.ALTERNATE_TITLES_LOCK, metadata.alternateTitlesLock)
|
||||
.set(d.LAST_MODIFIED_DATE, LocalDateTime.now(ZoneId.of("Z")))
|
||||
.where(d.SERIES_ID.eq(metadata.seriesId))
|
||||
.execute()
|
||||
|
|
@ -139,10 +152,15 @@ class SeriesMetadataDao(
|
|||
.where(slk.SERIES_ID.eq(metadata.seriesId))
|
||||
.execute()
|
||||
|
||||
dsl.deleteFrom(sat)
|
||||
.where(sat.SERIES_ID.eq(metadata.seriesId))
|
||||
.execute()
|
||||
|
||||
insertGenres(metadata)
|
||||
insertTags(metadata)
|
||||
insertSharingLabels(metadata)
|
||||
insertLinks(metadata)
|
||||
insertAlternateTitles(metadata)
|
||||
}
|
||||
|
||||
private fun insertGenres(metadata: SeriesMetadata) {
|
||||
|
|
@ -205,12 +223,28 @@ class SeriesMetadataDao(
|
|||
}
|
||||
}
|
||||
|
||||
private fun insertAlternateTitles(metadata: SeriesMetadata) {
|
||||
if (metadata.alternateTitles.isNotEmpty()) {
|
||||
metadata.alternateTitles.chunked(batchSize).forEach { chunk ->
|
||||
dsl.batch(
|
||||
dsl.insertInto(sat, sat.SERIES_ID, sat.LABEL, sat.TITLE)
|
||||
.values(null as String?, null, null),
|
||||
).also { step ->
|
||||
chunk.forEach {
|
||||
step.bind(metadata.seriesId, it.label, it.title)
|
||||
}
|
||||
}.execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
override fun delete(seriesId: String) {
|
||||
dsl.deleteFrom(g).where(g.SERIES_ID.eq(seriesId)).execute()
|
||||
dsl.deleteFrom(st).where(st.SERIES_ID.eq(seriesId)).execute()
|
||||
dsl.deleteFrom(sl).where(sl.SERIES_ID.eq(seriesId)).execute()
|
||||
dsl.deleteFrom(slk).where(slk.SERIES_ID.eq(seriesId)).execute()
|
||||
dsl.deleteFrom(sat).where(sat.SERIES_ID.eq(seriesId)).execute()
|
||||
dsl.deleteFrom(d).where(d.SERIES_ID.eq(seriesId)).execute()
|
||||
}
|
||||
|
||||
|
|
@ -222,12 +256,13 @@ class SeriesMetadataDao(
|
|||
dsl.deleteFrom(st).where(st.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
|
||||
dsl.deleteFrom(sl).where(sl.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
|
||||
dsl.deleteFrom(slk).where(slk.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
|
||||
dsl.deleteFrom(sat).where(sat.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
|
||||
dsl.deleteFrom(d).where(d.SERIES_ID.`in`(dsl.selectTempStrings())).execute()
|
||||
}
|
||||
|
||||
override fun count(): Long = dsl.fetchCount(d).toLong()
|
||||
|
||||
private fun SeriesMetadataRecord.toDomain(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLink>) =
|
||||
private fun SeriesMetadataRecord.toDomain(genres: Set<String>, tags: Set<String>, sharingLabels: Set<String>, links: List<WebLink>, alternateTitles: List<AlternateTitle>) =
|
||||
SeriesMetadata(
|
||||
status = SeriesMetadata.Status.valueOf(status),
|
||||
title = title,
|
||||
|
|
@ -244,6 +279,7 @@ class SeriesMetadataDao(
|
|||
totalBookCount = totalBookCount,
|
||||
sharingLabels = sharingLabels,
|
||||
links = links,
|
||||
alternateTitles = alternateTitles,
|
||||
|
||||
statusLock = statusLock,
|
||||
titleLock = titleLock,
|
||||
|
|
@ -258,6 +294,7 @@ class SeriesMetadataDao(
|
|||
totalBookCountLock = totalBookCountLock,
|
||||
sharingLabelsLock = sharingLabelsLock,
|
||||
linksLock = linksLock,
|
||||
alternateTitlesLock = alternateTitlesLock,
|
||||
|
||||
seriesId = seriesId,
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ fun SeriesDto.toDocument() =
|
|||
Document().apply {
|
||||
add(TextField("title", metadata.title, Field.Store.NO))
|
||||
if (metadata.titleSort != metadata.title) add(TextField("title", metadata.titleSort, Field.Store.NO))
|
||||
metadata.alternateTitles.forEach { add(TextField("title", it.title, Field.Store.NO)) }
|
||||
add(TextField("publisher", metadata.publisher, Field.Store.NO))
|
||||
add(TextField("status", metadata.status, Field.Store.NO))
|
||||
add(TextField("reading_direction", metadata.readingDirection, Field.Store.NO))
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import org.gotson.komga.application.events.EventPublisher
|
|||
import org.gotson.komga.application.tasks.HIGHEST_PRIORITY
|
||||
import org.gotson.komga.application.tasks.HIGH_PRIORITY
|
||||
import org.gotson.komga.application.tasks.TaskEmitter
|
||||
import org.gotson.komga.domain.model.AlternateTitle
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
|
|
@ -550,6 +551,10 @@ class SeriesController(
|
|||
if (links != null) links!!.map { WebLink(it.label!!, URI(it.url!!)) } else emptyList()
|
||||
} else existing.links,
|
||||
linksLock = linksLock ?: existing.linksLock,
|
||||
alternateTitles = if (isSet("alternateTitles")) {
|
||||
if (alternateTitles != null) alternateTitles!!.map { AlternateTitle(it.label!!, it.title!!) } else emptyList()
|
||||
} else existing.alternateTitles,
|
||||
alternateTitlesLock = alternateTitlesLock ?: existing.alternateTitlesLock,
|
||||
)
|
||||
}
|
||||
seriesMetadataRepository.update(updated)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import org.gotson.komga.domain.model.AlternateTitle
|
||||
|
||||
data class AlternateTitleDto(
|
||||
val label: String,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
fun AlternateTitle.toDto() = AlternateTitleDto(label, title)
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package org.gotson.komga.interfaces.api.rest.dto
|
||||
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
class AlternateTitleUpdateDto {
|
||||
@get:NotBlank
|
||||
val label: String? = null
|
||||
|
||||
@get:NotBlank
|
||||
val title: String? = null
|
||||
}
|
||||
|
|
@ -54,6 +54,8 @@ data class SeriesMetadataDto(
|
|||
val sharingLabelsLock: Boolean,
|
||||
val links: List<WebLinkDto>,
|
||||
val linksLock: Boolean,
|
||||
val alternateTitles: List<AlternateTitleDto>,
|
||||
val alternateTitlesLock: Boolean,
|
||||
|
||||
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
|
||||
val created: LocalDateTime,
|
||||
|
|
|
|||
|
|
@ -90,4 +90,12 @@ class SeriesMetadataUpdateDto {
|
|||
}
|
||||
|
||||
var linksLock: Boolean? = null
|
||||
|
||||
@get:Valid
|
||||
var alternateTitles: List<AlternateTitleUpdateDto>?
|
||||
by Delegates.observable(null) { prop, _, _ ->
|
||||
isSet[prop.name] = true
|
||||
}
|
||||
|
||||
var alternateTitlesLock: Boolean? = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package org.gotson.komga.infrastructure.jooq
|
|||
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.catchThrowable
|
||||
import org.gotson.komga.domain.model.AlternateTitle
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.WebLink
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
|
|
@ -66,6 +67,7 @@ class SeriesMetadataDaoTest(
|
|||
totalBookCount = 5,
|
||||
sharingLabels = setOf("kids"),
|
||||
links = listOf(WebLink("Comicvine", URI("https://comicvine.gamespot.com/doctor-strange/4050-2676/"))),
|
||||
alternateTitles = listOf(AlternateTitle("fr", "La Series")),
|
||||
titleLock = true,
|
||||
titleSortLock = true,
|
||||
summaryLock = true,
|
||||
|
|
@ -78,6 +80,7 @@ class SeriesMetadataDaoTest(
|
|||
totalBookCountLock = true,
|
||||
sharingLabelsLock = true,
|
||||
linksLock = true,
|
||||
alternateTitlesLock = true,
|
||||
seriesId = series.id,
|
||||
)
|
||||
|
||||
|
|
@ -104,6 +107,10 @@ class SeriesMetadataDaoTest(
|
|||
assertThat(label).isEqualTo(metadata.links.first().label)
|
||||
assertThat(url).isEqualTo(metadata.links.first().url)
|
||||
}
|
||||
with(created.alternateTitles.first()) {
|
||||
assertThat(label).isEqualTo(metadata.alternateTitles.first().label)
|
||||
assertThat(title).isEqualTo(metadata.alternateTitles.first().title)
|
||||
}
|
||||
|
||||
assertThat(created.titleLock).isEqualTo(metadata.titleLock)
|
||||
assertThat(created.titleSortLock).isEqualTo(metadata.titleSortLock)
|
||||
|
|
@ -118,6 +125,7 @@ class SeriesMetadataDaoTest(
|
|||
assertThat(created.totalBookCountLock).isEqualTo(metadata.totalBookCountLock)
|
||||
assertThat(created.sharingLabelsLock).isEqualTo(metadata.sharingLabelsLock)
|
||||
assertThat(created.linksLock).isEqualTo(metadata.linksLock)
|
||||
assertThat(created.alternateTitlesLock).isEqualTo(metadata.alternateTitlesLock)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -150,6 +158,7 @@ class SeriesMetadataDaoTest(
|
|||
assertThat(created.totalBookCount).isNull()
|
||||
assertThat(created.sharingLabels).isEmpty()
|
||||
assertThat(created.links).isEmpty()
|
||||
assertThat(created.alternateTitles).isEmpty()
|
||||
|
||||
assertThat(created.titleLock).isFalse
|
||||
assertThat(created.titleSortLock).isFalse
|
||||
|
|
@ -164,6 +173,7 @@ class SeriesMetadataDaoTest(
|
|||
assertThat(created.totalBookCountLock).isFalse
|
||||
assertThat(created.sharingLabelsLock).isFalse
|
||||
assertThat(created.linksLock).isFalse
|
||||
assertThat(created.alternateTitlesLock).isFalse
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -217,6 +227,7 @@ class SeriesMetadataDaoTest(
|
|||
totalBookCount = 3,
|
||||
sharingLabels = setOf("kids"),
|
||||
links = listOf(WebLink("Comicvine", URI("https://comicvine.gamespot.com/doctor-strange/4050-2676/"))),
|
||||
alternateTitles = listOf(AlternateTitle("fr", "La Series")),
|
||||
seriesId = series.id,
|
||||
)
|
||||
seriesMetadataDao.insert(metadata)
|
||||
|
|
@ -239,6 +250,7 @@ class SeriesMetadataDaoTest(
|
|||
totalBookCount = 8,
|
||||
sharingLabels = setOf("adult"),
|
||||
links = emptyList(),
|
||||
alternateTitles = emptyList(),
|
||||
statusLock = true,
|
||||
titleLock = true,
|
||||
titleSortLock = true,
|
||||
|
|
@ -252,6 +264,7 @@ class SeriesMetadataDaoTest(
|
|||
totalBookCountLock = true,
|
||||
sharingLabelsLock = true,
|
||||
linksLock = true,
|
||||
alternateTitlesLock = true,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -276,6 +289,7 @@ class SeriesMetadataDaoTest(
|
|||
assertThat(modified.totalBookCount).isEqualTo(updated.totalBookCount)
|
||||
assertThat(modified.sharingLabels).containsAll(updated.sharingLabels)
|
||||
assertThat(modified.links).isEmpty()
|
||||
assertThat(modified.alternateTitles).isEmpty()
|
||||
|
||||
assertThat(modified.titleLock).isTrue
|
||||
assertThat(modified.titleSortLock).isTrue
|
||||
|
|
@ -290,5 +304,6 @@ class SeriesMetadataDaoTest(
|
|||
assertThat(modified.totalBookCountLock).isTrue
|
||||
assertThat(modified.sharingLabelsLock).isTrue
|
||||
assertThat(modified.linksLock).isTrue
|
||||
assertThat(modified.alternateTitlesLock).isTrue
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue