mirror of
https://github.com/gotson/komga.git
synced 2025-12-19 23:12:47 +01:00
feat(webui): custom cover upload
Closes #473 Co-authored-by: Gauthier Roebroeck <gauthier.roebroeck@gmail.com>
This commit is contained in:
parent
9871487194
commit
2a56fffa9a
30 changed files with 914 additions and 141 deletions
|
|
@ -129,8 +129,18 @@ import {RawLocation} from 'vue-router'
|
|||
import ReadListActionsMenu from '@/components/menus/ReadListActionsMenu.vue'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
import {SeriesDto} from '@/types/komga-series'
|
||||
import {THUMBNAILBOOK_ADDED, THUMBNAILSERIES_ADDED} from '@/types/events'
|
||||
import {ThumbnailBookSseDto, ThumbnailSeriesSseDto} from '@/types/komga-sse'
|
||||
import {
|
||||
THUMBNAILBOOK_ADDED, THUMBNAILBOOK_DELETED,
|
||||
THUMBNAILCOLLECTION_ADDED, THUMBNAILCOLLECTION_DELETED,
|
||||
THUMBNAILREADLIST_ADDED, THUMBNAILREADLIST_DELETED,
|
||||
THUMBNAILSERIES_ADDED, THUMBNAILSERIES_DELETED,
|
||||
} from '@/types/events'
|
||||
import {
|
||||
ThumbnailBookSseDto,
|
||||
ThumbnailCollectionSseDto,
|
||||
ThumbnailReadListSseDto,
|
||||
ThumbnailSeriesSseDto,
|
||||
} from '@/types/komga-sse'
|
||||
import {coverBase64} from '@/types/image'
|
||||
|
||||
export default Vue.extend({
|
||||
|
|
@ -194,12 +204,30 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
created() {
|
||||
this.$eventHub.$on(THUMBNAILBOOK_ADDED, this.thumbnailBookAdded)
|
||||
this.$eventHub.$on(THUMBNAILSERIES_ADDED, this.thumbnailSeriesAdded)
|
||||
this.$eventHub.$on(THUMBNAILBOOK_ADDED, this.thumbnailBookChanged)
|
||||
this.$eventHub.$on(THUMBNAILBOOK_DELETED, this.thumbnailBookChanged)
|
||||
|
||||
this.$eventHub.$on(THUMBNAILSERIES_ADDED, this.thumbnailSeriesChanged)
|
||||
this.$eventHub.$on(THUMBNAILSERIES_DELETED, this.thumbnailSeriesChanged)
|
||||
|
||||
this.$eventHub.$on(THUMBNAILREADLIST_ADDED, this.thumbnailReadListChanged)
|
||||
this.$eventHub.$on(THUMBNAILREADLIST_DELETED, this.thumbnailReadListChanged)
|
||||
|
||||
this.$eventHub.$on(THUMBNAILCOLLECTION_ADDED, this.thumbnailCollectionChanged)
|
||||
this.$eventHub.$on(THUMBNAILCOLLECTION_DELETED, this.thumbnailCollectionChanged)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$eventHub.$off(THUMBNAILBOOK_ADDED, this.thumbnailBookAdded)
|
||||
this.$eventHub.$off(THUMBNAILSERIES_ADDED, this.thumbnailSeriesAdded)
|
||||
this.$eventHub.$off(THUMBNAILBOOK_ADDED, this.thumbnailBookChanged)
|
||||
this.$eventHub.$off(THUMBNAILBOOK_DELETED, this.thumbnailBookChanged)
|
||||
|
||||
this.$eventHub.$off(THUMBNAILSERIES_ADDED, this.thumbnailSeriesChanged)
|
||||
this.$eventHub.$off(THUMBNAILSERIES_DELETED, this.thumbnailSeriesChanged)
|
||||
|
||||
this.$eventHub.$off(THUMBNAILREADLIST_ADDED, this.thumbnailReadListChanged)
|
||||
this.$eventHub.$off(THUMBNAILREADLIST_DELETED, this.thumbnailReadListChanged)
|
||||
|
||||
this.$eventHub.$off(THUMBNAILCOLLECTION_ADDED, this.thumbnailCollectionChanged)
|
||||
this.$eventHub.$off(THUMBNAILCOLLECTION_DELETED, this.thumbnailCollectionChanged)
|
||||
},
|
||||
computed: {
|
||||
canReadPages(): boolean {
|
||||
|
|
@ -259,15 +287,25 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
thumbnailBookAdded(event: ThumbnailBookSseDto) {
|
||||
if (this.thumbnailError &&
|
||||
((this.computedItem.type() === ItemTypes.BOOK && event.bookId === this.item.id) || (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id))
|
||||
thumbnailBookChanged(event: ThumbnailBookSseDto) {
|
||||
if (event.selected && (this.computedItem.type() === ItemTypes.BOOK && event.bookId === this.item.id)
|
||||
|| (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id)
|
||||
) {
|
||||
this.thumbnailCacheBust = '?' + this.$_.random(1000)
|
||||
}
|
||||
},
|
||||
thumbnailSeriesAdded(event: ThumbnailSeriesSseDto) {
|
||||
if (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id) {
|
||||
thumbnailSeriesChanged(event: ThumbnailSeriesSseDto) {
|
||||
if (event.selected && this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id) {
|
||||
this.thumbnailCacheBust = '?' + this.$_.random(1000)
|
||||
}
|
||||
},
|
||||
thumbnailReadListChanged(event: ThumbnailReadListSseDto) {
|
||||
if (event.selected && this.computedItem.type() === ItemTypes.READLIST && event.readListId === this.item.id) {
|
||||
this.thumbnailCacheBust = '?' + this.$_.random(1000)
|
||||
}
|
||||
},
|
||||
thumbnailCollectionChanged(event: ThumbnailCollectionSseDto) {
|
||||
if (event.selected && this.computedItem.type() === ItemTypes.COLLECTION && event.collectionId === this.item.id) {
|
||||
this.thumbnailCacheBust = '?' + this.$_.random(1000)
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -56,7 +56,13 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import {SeriesThumbnailDto} from '@/types/komga-series'
|
||||
import {seriesThumbnailUrlByThumbnailId} from '@/functions/urls'
|
||||
import {
|
||||
bookThumbnailUrlByThumbnailId,
|
||||
collectionThumbnailUrlByThumbnailId,
|
||||
readListThumbnailUrlByThumbnailId,
|
||||
seriesThumbnailUrlByThumbnailId,
|
||||
} from '@/functions/urls'
|
||||
import {BookThumbnailDto} from '@/types/komga-books'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ThumbnailCard',
|
||||
|
|
@ -67,7 +73,7 @@ export default Vue.extend({
|
|||
if (value instanceof File) {
|
||||
return true
|
||||
}
|
||||
return 'id' in value && 'seriesId' in value && 'type' in value && 'selected' in value
|
||||
return 'id' in value && 'type' in value && 'selected' in value && ('seriesId' in value || 'bookId' in value || 'readListId' in value || 'collectionId' in value)
|
||||
},
|
||||
},
|
||||
selected: {
|
||||
|
|
@ -80,7 +86,7 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
getStatusIcon(item: File | SeriesThumbnailDto): string {
|
||||
getStatusIcon(item: File | SeriesThumbnailDto | BookThumbnailDto | ReadListThumbnailDto | CollectionThumbnailDto): string {
|
||||
if (item instanceof File) {
|
||||
if (this.isFileToBig(item)) {
|
||||
return 'mdi-alert-circle'
|
||||
|
|
@ -90,12 +96,14 @@ export default Vue.extend({
|
|||
} else {
|
||||
if (item.type === 'SIDECAR') {
|
||||
return 'mdi-folder-outline'
|
||||
} else if (item.type === 'GENERATED') {
|
||||
return 'mdi-file-outline'
|
||||
} else {
|
||||
return 'mdi-cloud-check-outline'
|
||||
}
|
||||
}
|
||||
},
|
||||
getStatusTooltip(item: File | SeriesThumbnailDto): string {
|
||||
getStatusTooltip(item: File | SeriesThumbnailDto | BookThumbnailDto | ReadListThumbnailDto | CollectionThumbnailDto): string {
|
||||
if (item instanceof File) {
|
||||
if (this.isFileToBig(item)) {
|
||||
return this.$t('thumbnail_card.tooltip_too_big').toString()
|
||||
|
|
@ -105,23 +113,34 @@ export default Vue.extend({
|
|||
} else {
|
||||
if (item.type === 'SIDECAR') {
|
||||
return this.$t('thumbnail_card.tooltip_sidecar').toString()
|
||||
}
|
||||
if (item.type === 'GENERATED') {
|
||||
return this.$t('thumbnail_card.tooltip_generated').toString()
|
||||
} else {
|
||||
return this.$t('thumbnail_card.tooltip_user_uploaded').toString()
|
||||
}
|
||||
}
|
||||
},
|
||||
isFileToBig(item: File | SeriesThumbnailDto): boolean {
|
||||
isFileToBig(item: File | SeriesThumbnailDto | BookThumbnailDto | ReadListThumbnailDto | CollectionThumbnailDto): boolean {
|
||||
if (item instanceof File) {
|
||||
return item.size > 1_000_000
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
getImage(item: File | SeriesThumbnailDto): string {
|
||||
getImage(item: File | SeriesThumbnailDto | BookThumbnailDto | ReadListThumbnailDto | CollectionThumbnailDto): string {
|
||||
if (item instanceof File) {
|
||||
return URL.createObjectURL(item)
|
||||
} else {
|
||||
} else if ('seriesId' in item) {
|
||||
return seriesThumbnailUrlByThumbnailId(item.seriesId, item.id)
|
||||
} else if ('bookId' in item) {
|
||||
return bookThumbnailUrlByThumbnailId(item.bookId, item.id)
|
||||
} else if ('readListId' in item) {
|
||||
return readListThumbnailUrlByThumbnailId(item.readListId, item.id)
|
||||
} else if ('collectionId' in item) {
|
||||
return collectionThumbnailUrlByThumbnailId(item.collectionId, item.id)
|
||||
} else {
|
||||
throw new Error('The given item type is not known!')
|
||||
}
|
||||
},
|
||||
onClickSelect() {
|
||||
|
|
@ -129,11 +148,11 @@ export default Vue.extend({
|
|||
this.$emit('on-select-thumbnail', this.item)
|
||||
}
|
||||
},
|
||||
isDeletable(item: File | SeriesThumbnailDto) {
|
||||
isDeletable(item: File | SeriesThumbnailDto | BookThumbnailDto | ReadListThumbnailDto | CollectionThumbnailDto) {
|
||||
if (item instanceof File) {
|
||||
return true
|
||||
} else {
|
||||
return item.type !== 'SIDECAR'
|
||||
return item.type !== 'SIDECAR' && item.type !== 'GENERATED'
|
||||
}
|
||||
},
|
||||
onClickDelete() {
|
||||
|
|
|
|||
|
|
@ -1,65 +1,128 @@
|
|||
<template>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
:fullscreen="$vuetify.breakpoint.xsOnly"
|
||||
max-width="800"
|
||||
@keydown.esc="dialogCancel"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('dialog.edit_collection.dialog_title') }}</v-card-title>
|
||||
<form novalidate>
|
||||
<v-card>
|
||||
<v-card-title class="hidden-xs-only">
|
||||
<v-icon class="mx-4">mdi-pencil</v-icon>
|
||||
{{ $t('dialog.edit_collection.dialog_title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.name"
|
||||
label="Name"
|
||||
:error-messages="getErrorsName"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<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_collection.tab_general') }}
|
||||
</v-tab>
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-image</v-icon>
|
||||
{{ $t('dialog.edit_collection.tab_poster') }}
|
||||
</v-tab>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-body-2">{{ $t('dialog.edit_collection.label_ordering') }}</div>
|
||||
<v-checkbox
|
||||
v-model="form.ordered"
|
||||
:label="$t('dialog.edit_collection.field_manual_ordering')"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- Tab: General -->
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-container fluid>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.name"
|
||||
label="Name"
|
||||
:error-messages="getErrorsName"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-body-2">{{ $t('dialog.edit_collection.label_ordering') }}</div>
|
||||
<v-checkbox
|
||||
v-model="form.ordered"
|
||||
:label="$t('dialog.edit_collection.field_manual_ordering')"
|
||||
hide-details
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogCancel">{{ $t('dialog.edit_collection.button_cancel') }}</v-btn>
|
||||
<v-btn color="primary"
|
||||
@click="dialogConfirm"
|
||||
:disabled="getErrorsName !== ''"
|
||||
>{{ $t('dialog.edit_collection.button_confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Tab: Thumbnails -->
|
||||
<v-tab-item>
|
||||
<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.collectionThumbnails]"
|
||||
: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>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogCancel">{{ $t('dialog.edit_collection.button_cancel') }}</v-btn>
|
||||
<v-btn color="primary"
|
||||
@click="dialogConfirm"
|
||||
:disabled="getErrorsName !== ''"
|
||||
>{{ $t('dialog.edit_collection.button_confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {UserRoles} from '@/types/enum-users'
|
||||
import Vue from 'vue'
|
||||
import {ERROR} from '@/types/events'
|
||||
import {ERROR, ErrorEvent} from '@/types/events'
|
||||
import {LibraryDto} from '@/types/komga-libraries'
|
||||
import ThumbnailCard from '@/components/ThumbnailCard.vue'
|
||||
import DropZone from '@/components/DropZone.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CollectionEditDialog',
|
||||
components: {ThumbnailCard, DropZone},
|
||||
data: () => {
|
||||
return {
|
||||
UserRoles,
|
||||
modal: false,
|
||||
tab: 0,
|
||||
collections: [] as CollectionDto[],
|
||||
form: {
|
||||
name: '',
|
||||
ordered: false,
|
||||
},
|
||||
poster: {
|
||||
selectedThumbnail: '',
|
||||
uploadQueue: [] as File[],
|
||||
deleteQueue: [] as CollectionThumbnailDto[],
|
||||
collectionThumbnails: [] as CollectionThumbnailDto[],
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
|
@ -79,6 +142,7 @@ export default Vue.extend({
|
|||
},
|
||||
modal(val) {
|
||||
!val && this.dialogCancel()
|
||||
val && this.getThumbnails(this.collection)
|
||||
},
|
||||
collection: {
|
||||
handler(val) {
|
||||
|
|
@ -101,8 +165,14 @@ export default Vue.extend({
|
|||
},
|
||||
methods: {
|
||||
async dialogReset(collection: CollectionDto) {
|
||||
this.tab = 0
|
||||
this.form.name = collection.name
|
||||
this.form.ordered = collection.ordered
|
||||
|
||||
this.poster.selectedThumbnail = ''
|
||||
this.poster.deleteQueue = []
|
||||
this.poster.uploadQueue = []
|
||||
this.poster.collectionThumbnails = []
|
||||
},
|
||||
dialogCancel() {
|
||||
this.$emit('input', false)
|
||||
|
|
@ -113,6 +183,34 @@ export default Vue.extend({
|
|||
},
|
||||
async editCollection() {
|
||||
try {
|
||||
if (this.poster.uploadQueue.length > 0) {
|
||||
let hadErrors = false
|
||||
for (const file of this.poster.uploadQueue.slice()) {
|
||||
try {
|
||||
await this.$komgaCollections.uploadThumbnail(this.collection.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(this.collection)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (this.poster.selectedThumbnail !== '') {
|
||||
const id = this.poster.selectedThumbnail
|
||||
if (this.poster.collectionThumbnails.find(value => value.id === id)) {
|
||||
await this.$komgaCollections.markThumbnailAsSelected(this.collection.id, id)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.poster.deleteQueue.length > 0) {
|
||||
this.poster.deleteQueue.forEach(toDelete => this.$komgaCollections.deleteThumbnail(toDelete.collectionId, toDelete.id))
|
||||
}
|
||||
|
||||
const update = {
|
||||
name: this.form.name,
|
||||
ordered: this.form.ordered,
|
||||
|
|
@ -123,6 +221,71 @@ export default Vue.extend({
|
|||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
},
|
||||
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(readList: CollectionDto) {
|
||||
const thumbnails = await this.$komgaCollections.getThumbnails(readList.id)
|
||||
|
||||
this.selectThumbnail(thumbnails.find(x => x.selected))
|
||||
|
||||
this.poster.collectionThumbnails = thumbnails
|
||||
},
|
||||
isThumbnailSelected(item: File | CollectionThumbnailDto): boolean {
|
||||
return item instanceof File ? item.name === this.poster.selectedThumbnail : item.id === this.poster.selectedThumbnail
|
||||
},
|
||||
selectThumbnail(item: File | CollectionThumbnailDto | 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 | CollectionThumbnailDto) {
|
||||
if (item instanceof File) {
|
||||
return false
|
||||
} else {
|
||||
return this.poster.deleteQueue.includes(item)
|
||||
}
|
||||
},
|
||||
deleteThumbnail(item: File | CollectionThumbnailDto) {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@
|
|||
<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>
|
||||
|
||||
<!-- Tab: General -->
|
||||
<v-tab-item v-if="single">
|
||||
|
|
@ -357,6 +361,37 @@
|
|||
</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>
|
||||
</v-tabs>
|
||||
|
||||
<v-card-actions class="hidden-xs-only">
|
||||
|
|
@ -374,16 +409,19 @@ import {groupAuthorsByRole} from '@/functions/authors'
|
|||
import {authorRoles} from '@/types/author-roles'
|
||||
import Vue from 'vue'
|
||||
import {helpers, requiredIf} from 'vuelidate/lib/validators'
|
||||
import {BookDto} from '@/types/komga-books'
|
||||
import {BookDto, BookThumbnailDto} from '@/types/komga-books'
|
||||
import IsbnVerify from '@saekitominaga/isbn-verify'
|
||||
import {isMatch} from 'date-fns'
|
||||
import {ERROR} from '@/types/events'
|
||||
import {ERROR, ErrorEvent} from '@/types/events'
|
||||
import DropZone from '@/components/DropZone.vue'
|
||||
import ThumbnailCard from '@/components/ThumbnailCard.vue'
|
||||
|
||||
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: 'EditBooksDialog',
|
||||
components: {ThumbnailCard, DropZone},
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
|
|
@ -412,6 +450,12 @@ export default Vue.extend({
|
|||
links: [],
|
||||
linksLock: false,
|
||||
},
|
||||
poster: {
|
||||
selectedThumbnail: '',
|
||||
uploadQueue: [] as File[],
|
||||
deleteQueue: [] as BookThumbnailDto[],
|
||||
bookThumbnails: [] as BookThumbnailDto[],
|
||||
},
|
||||
authorSearch: [],
|
||||
authorSearchResults: [] as string[],
|
||||
tagsAvailable: [] as string[],
|
||||
|
|
@ -430,6 +474,7 @@ export default Vue.extend({
|
|||
},
|
||||
modal(val) {
|
||||
!val && this.dialogCancel()
|
||||
val && this.getThumbnails(this.books)
|
||||
},
|
||||
books: {
|
||||
immediate: true,
|
||||
|
|
@ -571,6 +616,10 @@ export default Vue.extend({
|
|||
const book = books as BookDto
|
||||
this.$_.merge(this.form, book.metadata)
|
||||
this.form.authors = groupAuthorsByRole(book.metadata.authors)
|
||||
this.poster.selectedThumbnail = ''
|
||||
this.poster.deleteQueue = []
|
||||
this.poster.uploadQueue = []
|
||||
this.poster.bookThumbnails = []
|
||||
}
|
||||
},
|
||||
dialogCancel() {
|
||||
|
|
@ -646,6 +695,36 @@ export default Vue.extend({
|
|||
return null
|
||||
},
|
||||
async editBooks(): 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.books] : this.books) as BookDto[]
|
||||
|
|
@ -659,6 +738,73 @@ export default Vue.extend({
|
|||
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>
|
||||
|
|
|
|||
|
|
@ -737,6 +737,9 @@ export default Vue.extend({
|
|||
} 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
|
||||
}
|
||||
},
|
||||
|
|
@ -765,6 +768,7 @@ export default Vue.extend({
|
|||
}
|
||||
} else {
|
||||
this.poster.deleteQueue.push(item)
|
||||
if (item.id === this.poster.selectedThumbnail) this.poster.selectedThumbnail = ''
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,66 +1,130 @@
|
|||
<template>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
:fullscreen="$vuetify.breakpoint.xsOnly"
|
||||
max-width="800"
|
||||
@keydown.esc="dialogCancel"
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('dialog.edit_readlist.dialog_title') }}</v-card-title>
|
||||
<form novalidate>
|
||||
<v-card>
|
||||
<v-card-title class="hidden-xs-only">
|
||||
<v-icon class="mx-4">mdi-pencil</v-icon>
|
||||
{{ $t('dialog.edit_readlist.dialog_title') }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<v-container fluid>
|
||||
<!-- Name -->
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.name"
|
||||
:label="$t('dialog.edit_readlist.field_name')"
|
||||
:error-messages="getErrorsName"
|
||||
filled
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<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_readlist.tab_general') }}
|
||||
</v-tab>
|
||||
<v-tab class="justify-start">
|
||||
<v-icon left class="hidden-xs-only">mdi-image</v-icon>
|
||||
{{ $t('dialog.edit_readlist.tab_poster') }}
|
||||
</v-tab>
|
||||
|
||||
<!-- Summary -->
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="form.summary"
|
||||
:label="$t('dialog.edit_readlist.field_summary')"
|
||||
filled
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<!-- Tab: General -->
|
||||
<v-tab-item>
|
||||
<v-card flat>
|
||||
<v-container fluid>
|
||||
<!-- Name -->
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-text-field v-model="form.name"
|
||||
:label="$t('dialog.edit_readlist.field_name')"
|
||||
:error-messages="getErrorsName"
|
||||
filled
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<!-- Summary -->
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-textarea v-model="form.summary"
|
||||
:label="$t('dialog.edit_readlist.field_summary')"
|
||||
filled
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogCancel">{{ $t('dialog.edit_readlist.button_cancel') }}</v-btn>
|
||||
<v-btn color="primary"
|
||||
@click="dialogConfirm"
|
||||
:disabled="getErrorsName !== ''"
|
||||
>{{ $t('dialog.edit_readlist.button_confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-card>
|
||||
</v-tab-item>
|
||||
|
||||
<!-- Tab: Thumbnails -->
|
||||
<v-tab-item>
|
||||
<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.readListThumbnails]"
|
||||
: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>
|
||||
|
||||
</v-tabs>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer/>
|
||||
<v-btn text @click="dialogCancel">{{ $t('dialog.edit_readlist.button_cancel') }}</v-btn>
|
||||
<v-btn color="primary"
|
||||
@click="dialogConfirm"
|
||||
:disabled="getErrorsName !== ''"
|
||||
>{{ $t('dialog.edit_readlist.button_confirm') }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</form>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {UserRoles} from '@/types/enum-users'
|
||||
import Vue from 'vue'
|
||||
import {ERROR} from '@/types/events'
|
||||
import {ERROR, ErrorEvent} from '@/types/events'
|
||||
import {LibraryDto} from '@/types/komga-libraries'
|
||||
import DropZone from '@/components/DropZone.vue'
|
||||
import ThumbnailCard from '@/components/ThumbnailCard.vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ReadListEditDialog',
|
||||
components: {ThumbnailCard, DropZone},
|
||||
data: () => {
|
||||
return {
|
||||
UserRoles,
|
||||
modal: false,
|
||||
tab: 0,
|
||||
readLists: [] as ReadListDto[],
|
||||
form: {
|
||||
name: '',
|
||||
summary: '',
|
||||
},
|
||||
poster: {
|
||||
selectedThumbnail: '',
|
||||
uploadQueue: [] as File[],
|
||||
deleteQueue: [] as ReadListThumbnailDto[],
|
||||
readListThumbnails: [] as ReadListThumbnailDto[],
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
|
@ -80,6 +144,7 @@ export default Vue.extend({
|
|||
},
|
||||
modal(val) {
|
||||
!val && this.dialogCancel()
|
||||
val && this.getThumbnails(this.readList)
|
||||
},
|
||||
readList: {
|
||||
handler(val) {
|
||||
|
|
@ -102,8 +167,14 @@ export default Vue.extend({
|
|||
},
|
||||
methods: {
|
||||
async dialogReset(readList: ReadListDto) {
|
||||
this.tab = 0
|
||||
this.form.name = readList.name
|
||||
this.form.summary = readList.summary
|
||||
|
||||
this.poster.selectedThumbnail = ''
|
||||
this.poster.deleteQueue = []
|
||||
this.poster.uploadQueue = []
|
||||
this.poster.readListThumbnails = []
|
||||
},
|
||||
dialogCancel() {
|
||||
this.$emit('input', false)
|
||||
|
|
@ -114,6 +185,34 @@ export default Vue.extend({
|
|||
},
|
||||
async edit() {
|
||||
try {
|
||||
if (this.poster.uploadQueue.length > 0) {
|
||||
let hadErrors = false
|
||||
for (const file of this.poster.uploadQueue.slice()) {
|
||||
try {
|
||||
await this.$komgaReadLists.uploadThumbnail(this.readList.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(this.readList)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (this.poster.selectedThumbnail !== '') {
|
||||
const id = this.poster.selectedThumbnail
|
||||
if (this.poster.readListThumbnails.find(value => value.id === id)) {
|
||||
await this.$komgaReadLists.markThumbnailAsSelected(this.readList.id, id)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.poster.deleteQueue.length > 0) {
|
||||
this.poster.deleteQueue.forEach(toDelete => this.$komgaReadLists.deleteThumbnail(toDelete.readListId, toDelete.id))
|
||||
}
|
||||
|
||||
const update = {
|
||||
name: this.form.name,
|
||||
summary: this.form.summary,
|
||||
|
|
@ -124,6 +223,71 @@ export default Vue.extend({
|
|||
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
|
||||
}
|
||||
},
|
||||
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(readList: ReadListDto) {
|
||||
const thumbnails = await this.$komgaReadLists.getThumbnails(readList.id)
|
||||
|
||||
this.selectThumbnail(thumbnails.find(x => x.selected))
|
||||
|
||||
this.poster.readListThumbnails = thumbnails
|
||||
},
|
||||
isThumbnailSelected(item: File | ReadListThumbnailDto): boolean {
|
||||
return item instanceof File ? item.name === this.poster.selectedThumbnail : item.id === this.poster.selectedThumbnail
|
||||
},
|
||||
selectThumbnail(item: File | ReadListThumbnailDto | 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 | ReadListThumbnailDto) {
|
||||
if (item instanceof File) {
|
||||
return false
|
||||
} else {
|
||||
return this.poster.deleteQueue.includes(item)
|
||||
}
|
||||
},
|
||||
deleteThumbnail(item: File | ReadListThumbnailDto) {
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -12,15 +12,19 @@ const urls = {
|
|||
|
||||
export default urls
|
||||
|
||||
export function bookThumbnailUrl (bookId: string): string {
|
||||
export function bookThumbnailUrl(bookId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/books/${bookId}/thumbnail`
|
||||
}
|
||||
|
||||
export function bookFileUrl (bookId: string): string {
|
||||
export function bookThumbnailUrlByThumbnailId(bookId: string, thumbnailId: string) {
|
||||
return `${urls.originNoSlash}/api/v1/books/${bookId}/thumbnails/${thumbnailId}`
|
||||
}
|
||||
|
||||
export function bookFileUrl(bookId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/books/${bookId}/file`
|
||||
}
|
||||
|
||||
export function bookPageUrl (bookId: string, page: number, convertTo?: string): string {
|
||||
export function bookPageUrl(bookId: string, page: number, convertTo?: string): string {
|
||||
let url = `${urls.originNoSlash}/api/v1/books/${bookId}/pages/${page}`
|
||||
if (convertTo) {
|
||||
url += `?convert=${convertTo}`
|
||||
|
|
@ -28,15 +32,15 @@ export function bookPageUrl (bookId: string, page: number, convertTo?: string):
|
|||
return url
|
||||
}
|
||||
|
||||
export function bookPageThumbnailUrl (bookId: string, page: number): string {
|
||||
export function bookPageThumbnailUrl(bookId: string, page: number): string {
|
||||
return `${urls.originNoSlash}/api/v1/books/${bookId}/pages/${page}/thumbnail`
|
||||
}
|
||||
|
||||
export function seriesFileUrl (seriesId: string): string {
|
||||
export function seriesFileUrl(seriesId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/series/${seriesId}/file`
|
||||
}
|
||||
|
||||
export function seriesThumbnailUrl (seriesId: string): string {
|
||||
export function seriesThumbnailUrl(seriesId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/series/${seriesId}/thumbnail`
|
||||
}
|
||||
|
||||
|
|
@ -44,18 +48,26 @@ export function seriesThumbnailUrlByThumbnailId(seriesId: string, thumbnailId: s
|
|||
return `${urls.originNoSlash}/api/v1/series/${seriesId}/thumbnails/${thumbnailId}`
|
||||
}
|
||||
|
||||
export function collectionThumbnailUrl (collectionId: string): string {
|
||||
export function collectionThumbnailUrl(collectionId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/collections/${collectionId}/thumbnail`
|
||||
}
|
||||
|
||||
export function readListThumbnailUrl (readListId: string): string {
|
||||
export function collectionThumbnailUrlByThumbnailId(collectionId: string, thumbnailId: string) {
|
||||
return `${urls.originNoSlash}/api/v1/collections/${collectionId}/thumbnails/${thumbnailId}`
|
||||
}
|
||||
|
||||
export function readListThumbnailUrl(readListId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/readlists/${readListId}/thumbnail`
|
||||
}
|
||||
|
||||
export function readListFileUrl (readListId: string): string {
|
||||
export function readListFileUrl(readListId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/readlists/${readListId}/file`
|
||||
}
|
||||
|
||||
export function transientBookPageUrl (transientBookId: string, page: number): string {
|
||||
export function readListThumbnailUrlByThumbnailId(readListId: string, thumbnailId: string) {
|
||||
return `${urls.originNoSlash}/api/v1/readlists/${readListId}/thumbnails/${thumbnailId}`
|
||||
}
|
||||
|
||||
export function transientBookPageUrl(transientBookId: string, page: number): string {
|
||||
return `${urls.originNoSlash}/api/v1/transient-books/${transientBookId}/pages/${page}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -345,6 +345,7 @@
|
|||
"tab_authors": "Authors",
|
||||
"tab_general": "General",
|
||||
"tab_links": "Links",
|
||||
"tab_poster": "Poster",
|
||||
"tab_tags": "Tags",
|
||||
"tags_notice_multiple_edit": "You are editing tags for multiple books. This will override existing tags of each book."
|
||||
},
|
||||
|
|
@ -353,7 +354,9 @@
|
|||
"button_confirm": "Save changes",
|
||||
"dialog_title": "Edit collection",
|
||||
"field_manual_ordering": "Manual ordering",
|
||||
"label_ordering": "By default, series in a collection will be ordered by name. You can enable manual ordering to define your own order."
|
||||
"label_ordering": "By default, series in a collection will be ordered by name. You can enable manual ordering to define your own order.",
|
||||
"tab_general": "General",
|
||||
"tab_poster": "Poster"
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Browse",
|
||||
|
|
@ -405,7 +408,9 @@
|
|||
"button_confirm": "Save changes",
|
||||
"dialog_title": "Edit read list",
|
||||
"field_name": "Name",
|
||||
"field_summary": "Summary"
|
||||
"field_summary": "Summary",
|
||||
"tab_general": "General",
|
||||
"tab_poster": "Poster"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Cancel",
|
||||
|
|
@ -716,6 +721,7 @@
|
|||
},
|
||||
"thumbnail_card": {
|
||||
"tooltip_delete": "Delete",
|
||||
"tooltip_generated": "Generated artwork",
|
||||
"tooltip_mark_as_selected": "Mark as selected",
|
||||
"tooltip_selected": "Selected",
|
||||
"tooltip_sidecar": "Local artwork",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
BookImportBatchDto,
|
||||
BookMetadataUpdateBatchDto,
|
||||
BookMetadataUpdateDto,
|
||||
BookThumbnailDto,
|
||||
PageDto,
|
||||
ReadProgressUpdateDto,
|
||||
} from '@/types/komga-books'
|
||||
|
|
@ -239,4 +240,55 @@ export default class KomgaBooksService {
|
|||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getThumbnails(bookId: string): Promise<BookThumbnailDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_BOOKS}/${bookId}/thumbnails`)).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to retrieve thumbnails for book '${bookId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async uploadThumbnail(bookId: string, file: File, selected: boolean) {
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('file', file)
|
||||
body.append('selected', `${selected}`)
|
||||
await this.http.post(`${API_BOOKS}/${bookId}/thumbnails`, body)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to upload thumbnail for book '${bookId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteThumbnail(bookId: string, thumbnailId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_BOOKS}/${bookId}/thumbnails/${thumbnailId}`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to delete thumbnail for book '${bookId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async markThumbnailAsSelected(bookId: string, thumbnailId: string) {
|
||||
try {
|
||||
await this.http.put(`${API_BOOKS}/${bookId}/thumbnails/${thumbnailId}/selected`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to mark thumbnail as selected for book '${bookId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,58 @@ export default class KomgaCollectionsService {
|
|||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve series'
|
||||
let msg = `An error occurred while trying to retrieve series for collection '${collectionId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getThumbnails(collectionId: string): Promise<CollectionThumbnailDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_COLLECTIONS}/${collectionId}/thumbnails`)).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to retrieve thumbnails for collection '${collectionId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async uploadThumbnail(collecitonId: string, file: File, selected: boolean) {
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('file', file)
|
||||
body.append('selected', `${selected}`)
|
||||
await this.http.post(`${API_COLLECTIONS}/${collecitonId}/thumbnails`, body)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to upload thumbnail for collection '${collecitonId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteThumbnail(collectionId: string, thumbnailId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_COLLECTIONS}/${collectionId}/thumbnails/${thumbnailId}`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to delete thumbnail for collection '${collectionId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async markThumbnailAsSelected(collectionId: string, thumbnailId: string) {
|
||||
try {
|
||||
await this.http.put(`${API_COLLECTIONS}/${collectionId}/thumbnails/${thumbnailId}/selected`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to mark thumbnail as selected for collection '${collectionId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ export default class KomgaReadListsService {
|
|||
|
||||
return (await this.http.get(`${API_READLISTS}/${readListId}/books`, {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, { indices: false }),
|
||||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve books'
|
||||
|
|
@ -143,4 +143,55 @@ export default class KomgaReadListsService {
|
|||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getThumbnails(readListId: string): Promise<ReadListThumbnailDto[]> {
|
||||
try {
|
||||
return (await this.http.get(`${API_READLISTS}/${readListId}/thumbnails`)).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to retrieve thumbnails for readlist '${readListId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async uploadThumbnail(readListId: string, file: File, selected: boolean) {
|
||||
try {
|
||||
const body = new FormData()
|
||||
body.append('file', file)
|
||||
body.append('selected', `${selected}`)
|
||||
await this.http.post(`${API_READLISTS}/${readListId}/thumbnails`, body)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to upload thumbnail for readlist '${readListId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async deleteThumbnail(readListId: string, thumbnailId: string) {
|
||||
try {
|
||||
await this.http.delete(`${API_READLISTS}/${readListId}/thumbnails/${thumbnailId}`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to delete thumbnail for readlist '${readListId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async markThumbnailAsSelected(readListId: string, thumbnailId: string) {
|
||||
try {
|
||||
await this.http.put(`${API_READLISTS}/${readListId}/thumbnails/${thumbnailId}/selected`)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to mark thumbnail as selected for readlist '${readListId}'`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,13 @@ import {
|
|||
SERIES_CHANGED,
|
||||
SERIES_DELETED,
|
||||
THUMBNAILBOOK_ADDED,
|
||||
THUMBNAILBOOK_DELETED,
|
||||
THUMBNAILCOLLECTION_ADDED,
|
||||
THUMBNAILCOLLECTION_DELETED,
|
||||
THUMBNAILREADLIST_ADDED,
|
||||
THUMBNAILREADLIST_DELETED,
|
||||
THUMBNAILSERIES_ADDED,
|
||||
THUMBNAILSERIES_DELETED,
|
||||
} from '@/types/events'
|
||||
import Vue from 'vue'
|
||||
import {TaskQueueSseDto} from '@/types/komga-sse'
|
||||
|
|
@ -83,7 +89,16 @@ export default class KomgaSseService {
|
|||
|
||||
// Thumbnails
|
||||
this.eventSource.addEventListener('ThumbnailBookAdded', (event: any) => this.emit(THUMBNAILBOOK_ADDED, event))
|
||||
this.eventSource.addEventListener('ThumbnailBookDeleted', (event: any) => this.emit(THUMBNAILBOOK_DELETED, event))
|
||||
|
||||
this.eventSource.addEventListener('ThumbnailSeriesAdded', (event: any) => this.emit(THUMBNAILSERIES_ADDED, event))
|
||||
this.eventSource.addEventListener('ThumbnailSeriesDeleted', (event: any) => this.emit(THUMBNAILSERIES_DELETED, event))
|
||||
|
||||
this.eventSource.addEventListener('ThumbnailReadListAdded', (event: any) => this.emit(THUMBNAILREADLIST_ADDED, event))
|
||||
this.eventSource.addEventListener('ThumbnailReadListDeleted', (event: any) => this.emit(THUMBNAILREADLIST_DELETED, event))
|
||||
|
||||
this.eventSource.addEventListener('ThumbnailSeriesCollectionAdded', (event: any) => this.emit(THUMBNAILCOLLECTION_ADDED, event))
|
||||
this.eventSource.addEventListener('ThumbnailSeriesCollectionDeleted', (event: any) => this.emit(THUMBNAILCOLLECTION_DELETED, event))
|
||||
|
||||
this.eventSource.addEventListener('TaskQueueStatus', (event: any) => this.updateTaskCount(event))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,16 @@ export const READPROGRESS_SERIES_CHANGED = 'readprogress-series-changed'
|
|||
export const READPROGRESS_SERIES_DELETED = 'readprogress-series-deleted'
|
||||
|
||||
export const THUMBNAILBOOK_ADDED = 'thumbnailbook-added'
|
||||
export const THUMBNAILBOOK_DELETED = 'thumbnailbook-deleted'
|
||||
|
||||
export const THUMBNAILSERIES_ADDED = 'thumbnailseries-added'
|
||||
export const THUMBNAILSERIES_DELETED = 'thumbnailseries-deleted'
|
||||
|
||||
export const THUMBNAILREADLIST_ADDED = 'thumbnailreadlist-added'
|
||||
export const THUMBNAILREADLIST_DELETED = 'thumbnailreadlist-deleted'
|
||||
|
||||
export const THUMBNAILCOLLECTION_ADDED = 'thumbnailcollection-added'
|
||||
export const THUMBNAILCOLLECTION_DELETED = 'thumbnailcollection-deleted'
|
||||
|
||||
export const ERROR = 'error'
|
||||
export const NOTIFICATION = 'notification'
|
||||
|
|
|
|||
|
|
@ -134,3 +134,10 @@ export interface BookImportDto {
|
|||
upgradeBookId?: string,
|
||||
destinationName?: string,
|
||||
}
|
||||
|
||||
export interface BookThumbnailDto {
|
||||
id: string,
|
||||
bookId: string,
|
||||
type: string,
|
||||
selected: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,3 +19,10 @@ interface CollectionUpdateDto {
|
|||
ordered?: boolean,
|
||||
seriesIds?: string[]
|
||||
}
|
||||
|
||||
interface CollectionThumbnailDto {
|
||||
id: string,
|
||||
collectionId: string,
|
||||
type: string,
|
||||
selected: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,3 +36,10 @@ interface ReadListRequestBookDto {
|
|||
series: string,
|
||||
number: string,
|
||||
}
|
||||
|
||||
interface ReadListThumbnailDto {
|
||||
id: string,
|
||||
readListId: string,
|
||||
type: string,
|
||||
selected: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,10 +36,22 @@ export interface ReadProgressSeriesSseDto {
|
|||
export interface ThumbnailBookSseDto {
|
||||
bookId: string,
|
||||
seriesId: string,
|
||||
selected: boolean,
|
||||
}
|
||||
|
||||
export interface ThumbnailSeriesSseDto {
|
||||
seriesId: string,
|
||||
selected: boolean,
|
||||
}
|
||||
|
||||
export interface ThumbnailReadListSseDto {
|
||||
readListId: string,
|
||||
selected: boolean,
|
||||
}
|
||||
|
||||
export interface ThumbnailCollectionSseDto {
|
||||
collectionId: string,
|
||||
selected: boolean,
|
||||
}
|
||||
|
||||
export interface TaskQueueSseDto {
|
||||
|
|
|
|||
|
|
@ -128,23 +128,19 @@ class BookLifecycle(
|
|||
}
|
||||
}
|
||||
|
||||
when (markSelected) {
|
||||
MarkSelectedPreference.YES -> {
|
||||
thumbnailBookRepository.markSelected(thumbnail)
|
||||
}
|
||||
val selected = when (markSelected) {
|
||||
MarkSelectedPreference.YES -> true
|
||||
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
|
||||
val selectedThumbnail = thumbnailBookRepository.findSelectedByBookIdOrNull(thumbnail.bookId)
|
||||
|
||||
if (selectedThumbnail == null || selectedThumbnail.type == ThumbnailBook.Type.GENERATED)
|
||||
thumbnailBookRepository.markSelected(thumbnail)
|
||||
else thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
}
|
||||
MarkSelectedPreference.NO -> {
|
||||
thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
selectedThumbnail == null || selectedThumbnail.type == ThumbnailBook.Type.GENERATED
|
||||
}
|
||||
MarkSelectedPreference.NO -> false
|
||||
}
|
||||
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail))
|
||||
if (selected) thumbnailBookRepository.markSelected(thumbnail)
|
||||
else thumbnailsHouseKeeping(thumbnail.bookId)
|
||||
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(thumbnail.copy(selected = selected)))
|
||||
}
|
||||
|
||||
fun deleteThumbnailForBook(thumbnail: ThumbnailBook) {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ class ReadListLifecycle(
|
|||
|
||||
fun markSelectedThumbnail(thumbnail: ThumbnailReadList) {
|
||||
thumbnailReadListRepository.markSelected(thumbnail)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListAdded(thumbnail))
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListAdded(thumbnail.copy(selected = true)))
|
||||
}
|
||||
|
||||
fun deleteThumbnail(thumbnail: ThumbnailReadList) {
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ class SeriesCollectionLifecycle(
|
|||
|
||||
fun markSelectedThumbnail(thumbnail: ThumbnailSeriesCollection) {
|
||||
thumbnailSeriesCollectionRepository.markSelected(thumbnail)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesCollectionAdded(thumbnail))
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesCollectionAdded(thumbnail.copy(selected = true)))
|
||||
}
|
||||
|
||||
fun deleteThumbnail(thumbnail: ThumbnailSeriesCollection) {
|
||||
|
|
|
|||
|
|
@ -274,15 +274,17 @@ class SeriesLifecycle(
|
|||
}
|
||||
thumbnailsSeriesRepository.insert(thumbnail.copy(selected = false))
|
||||
|
||||
if (markSelected == MarkSelectedPreference.YES ||
|
||||
(
|
||||
markSelected == MarkSelectedPreference.IF_NONE_OR_GENERATED &&
|
||||
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
|
||||
)
|
||||
) {
|
||||
thumbnailsSeriesRepository.markSelected(thumbnail)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail))
|
||||
val selected = when (markSelected) {
|
||||
MarkSelectedPreference.YES -> true
|
||||
MarkSelectedPreference.IF_NONE_OR_GENERATED -> {
|
||||
thumbnailsSeriesRepository.findSelectedBySeriesIdOrNull(thumbnail.seriesId) == null
|
||||
}
|
||||
MarkSelectedPreference.NO -> false
|
||||
}
|
||||
|
||||
if (selected) thumbnailsSeriesRepository.markSelected(thumbnail)
|
||||
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(thumbnail.copy(selected = selected)))
|
||||
}
|
||||
|
||||
fun deleteThumbnailForSeries(thumbnail: ThumbnailSeries) {
|
||||
|
|
|
|||
|
|
@ -346,7 +346,7 @@ class BookController(
|
|||
|
||||
thumbnailBookRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
thumbnailBookRepository.markSelected(it)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(it))
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailBookAdded(it.copy(selected = true)))
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,8 +8,10 @@ import mu.KotlinLogging
|
|||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.gotson.komga.application.events.EventPublisher
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.BookSearchWithReadProgress
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
|
|
@ -86,6 +88,7 @@ class ReadListController(
|
|||
private val thumbnailReadListRepository: ThumbnailReadListRepository,
|
||||
private val contentDetector: ContentDetector,
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
private val eventPublisher: EventPublisher,
|
||||
) {
|
||||
|
||||
@PageableWithoutSortAsQueryParam
|
||||
|
|
@ -217,6 +220,7 @@ class ReadListController(
|
|||
readListRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
thumbnailReadListRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
readListLifecycle.markSelectedThumbnail(it)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailReadListAdded(it.copy(selected = true)))
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import io.swagger.v3.oas.annotations.media.Content
|
|||
import io.swagger.v3.oas.annotations.media.Schema
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.application.events.EventPublisher
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.DomainEvent
|
||||
import org.gotson.komga.domain.model.DuplicateNameException
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.model.ReadStatus
|
||||
|
|
@ -66,6 +68,7 @@ class SeriesCollectionController(
|
|||
private val seriesDtoRepository: SeriesDtoRepository,
|
||||
private val contentDetector: ContentDetector,
|
||||
private val thumbnailSeriesCollectionRepository: ThumbnailSeriesCollectionRepository,
|
||||
private val eventPublisher: EventPublisher,
|
||||
) {
|
||||
|
||||
@PageableWithoutSortAsQueryParam
|
||||
|
|
@ -179,6 +182,7 @@ class SeriesCollectionController(
|
|||
collectionRepository.findByIdOrNull(id, principal.user.getAuthorizedLibraryIds(null))?.let {
|
||||
thumbnailSeriesCollectionRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
collectionLifecycle.markSelectedThumbnail(it)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesCollectionAdded(it.copy(selected = true)))
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -407,7 +407,7 @@ class SeriesController(
|
|||
seriesRepository.findByIdOrNull(seriesId) ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
thumbnailsSeriesRepository.findByIdOrNull(thumbnailId)?.let {
|
||||
thumbnailsSeriesRepository.markSelected(it)
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(it))
|
||||
eventPublisher.publishEvent(DomainEvent.ThumbnailSeriesAdded(it.copy(selected = true)))
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -101,14 +101,14 @@ class SseController(
|
|||
is DomainEvent.ReadProgressSeriesChanged -> emitSse("ReadProgressSeriesChanged", ReadProgressSeriesSseDto(event.seriesId, event.userId), userIdOnly = event.userId)
|
||||
is DomainEvent.ReadProgressSeriesDeleted -> emitSse("ReadProgressSeriesDeleted", ReadProgressSeriesSseDto(event.seriesId, event.userId), userIdOnly = event.userId)
|
||||
|
||||
is DomainEvent.ThumbnailBookAdded -> emitSse("ThumbnailBookAdded", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty()))
|
||||
is DomainEvent.ThumbnailBookDeleted -> emitSse("ThumbnailBookDeleted", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty()))
|
||||
is DomainEvent.ThumbnailSeriesAdded -> emitSse("ThumbnailSeriesAdded", ThumbnailSeriesSseDto(event.thumbnail.seriesId))
|
||||
is DomainEvent.ThumbnailSeriesDeleted -> emitSse("ThumbnailSeriesDeleted", ThumbnailSeriesSseDto(event.thumbnail.seriesId))
|
||||
is DomainEvent.ThumbnailSeriesCollectionAdded -> emitSse("ThumbnailSeriesCollectionAdded", ThumbnailSeriesCollectionSseDto(event.thumbnail.collectionId))
|
||||
is DomainEvent.ThumbnailSeriesCollectionDeleted -> emitSse("ThumbnailSeriesCollectionDeleted", ThumbnailSeriesCollectionSseDto(event.thumbnail.collectionId))
|
||||
is DomainEvent.ThumbnailReadListAdded -> emitSse("ThumbnailReadListAdded", ThumbnailReadListSseDto(event.thumbnail.readListId))
|
||||
is DomainEvent.ThumbnailReadListDeleted -> emitSse("ThumbnailReadListDeleted", ThumbnailReadListSseDto(event.thumbnail.readListId))
|
||||
is DomainEvent.ThumbnailBookAdded -> emitSse("ThumbnailBookAdded", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty(), event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailBookDeleted -> emitSse("ThumbnailBookDeleted", ThumbnailBookSseDto(event.thumbnail.bookId, bookRepository.getSeriesIdOrNull(event.thumbnail.bookId).orEmpty(), event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailSeriesAdded -> emitSse("ThumbnailSeriesAdded", ThumbnailSeriesSseDto(event.thumbnail.seriesId, event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailSeriesDeleted -> emitSse("ThumbnailSeriesDeleted", ThumbnailSeriesSseDto(event.thumbnail.seriesId, event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailSeriesCollectionAdded -> emitSse("ThumbnailSeriesCollectionAdded", ThumbnailSeriesCollectionSseDto(event.thumbnail.collectionId, event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailSeriesCollectionDeleted -> emitSse("ThumbnailSeriesCollectionDeleted", ThumbnailSeriesCollectionSseDto(event.thumbnail.collectionId, event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailReadListAdded -> emitSse("ThumbnailReadListAdded", ThumbnailReadListSseDto(event.thumbnail.readListId, event.thumbnail.selected))
|
||||
is DomainEvent.ThumbnailReadListDeleted -> emitSse("ThumbnailReadListDeleted", ThumbnailReadListSseDto(event.thumbnail.readListId, event.thumbnail.selected))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,4 +3,5 @@ package org.gotson.komga.interfaces.sse.dto
|
|||
data class ThumbnailBookSseDto(
|
||||
val bookId: String,
|
||||
val seriesId: String,
|
||||
val selected: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ package org.gotson.komga.interfaces.sse.dto
|
|||
|
||||
data class ThumbnailReadListSseDto(
|
||||
val readListId: String,
|
||||
val selected: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ package org.gotson.komga.interfaces.sse.dto
|
|||
|
||||
data class ThumbnailSeriesCollectionSseDto(
|
||||
val collectionId: String,
|
||||
val selected: Boolean,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ package org.gotson.komga.interfaces.sse.dto
|
|||
|
||||
data class ThumbnailSeriesSseDto(
|
||||
val seriesId: String,
|
||||
val selected: Boolean,
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue