diff --git a/komga-webui/src/components/ItemCard.vue b/komga-webui/src/components/ItemCard.vue index 908994244..59f61a20c 100644 --- a/komga-webui/src/components/ItemCard.vue +++ b/komga-webui/src/components/ItemCard.vue @@ -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) } }, diff --git a/komga-webui/src/components/ThumbnailCard.vue b/komga-webui/src/components/ThumbnailCard.vue index 5a59a486a..4c5dc8fa8 100644 --- a/komga-webui/src/components/ThumbnailCard.vue +++ b/komga-webui/src/components/ThumbnailCard.vue @@ -56,7 +56,13 @@ diff --git a/komga-webui/src/components/dialogs/EditBooksDialog.vue b/komga-webui/src/components/dialogs/EditBooksDialog.vue index 4b6f06364..a3739b3f0 100644 --- a/komga-webui/src/components/dialogs/EditBooksDialog.vue +++ b/komga-webui/src/components/dialogs/EditBooksDialog.vue @@ -39,6 +39,10 @@ mdi-link {{ $t('dialog.edit_books.tab_links') }} + + mdi-image + {{ $t('dialog.edit_books.tab_poster') }} + @@ -357,6 +361,37 @@ + + + + + + + + + + + + + + + + + + + + + @@ -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 { + 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 = '' + } + } + }, }, }) diff --git a/komga-webui/src/components/dialogs/EditSeriesDialog.vue b/komga-webui/src/components/dialogs/EditSeriesDialog.vue index 2a32954f9..6334417ec 100644 --- a/komga-webui/src/components/dialogs/EditSeriesDialog.vue +++ b/komga-webui/src/components/dialogs/EditSeriesDialog.vue @@ -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 = '' } } }, diff --git a/komga-webui/src/components/dialogs/ReadListEditDialog.vue b/komga-webui/src/components/dialogs/ReadListEditDialog.vue index 94c2eee9f..4e860e07a 100644 --- a/komga-webui/src/components/dialogs/ReadListEditDialog.vue +++ b/komga-webui/src/components/dialogs/ReadListEditDialog.vue @@ -1,66 +1,130 @@ diff --git a/komga-webui/src/functions/urls.ts b/komga-webui/src/functions/urls.ts index e592aa0c3..a54774c1e 100644 --- a/komga-webui/src/functions/urls.ts +++ b/komga-webui/src/functions/urls.ts @@ -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}` } diff --git a/komga-webui/src/locales/en.json b/komga-webui/src/locales/en.json index f7dfd093e..3e63dc2f1 100644 --- a/komga-webui/src/locales/en.json +++ b/komga-webui/src/locales/en.json @@ -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", diff --git a/komga-webui/src/services/komga-books.service.ts b/komga-webui/src/services/komga-books.service.ts index d97af4d09..9db5211f6 100644 --- a/komga-webui/src/services/komga-books.service.ts +++ b/komga-webui/src/services/komga-books.service.ts @@ -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 { + 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) + } + } } diff --git a/komga-webui/src/services/komga-collections.service.ts b/komga-webui/src/services/komga-collections.service.ts index 332495d17..951639b42 100644 --- a/komga-webui/src/services/komga-collections.service.ts +++ b/komga-webui/src/services/komga-collections.service.ts @@ -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 { + 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}` } diff --git a/komga-webui/src/services/komga-readlists.service.ts b/komga-webui/src/services/komga-readlists.service.ts index e7d259a41..4aa040c00 100644 --- a/komga-webui/src/services/komga-readlists.service.ts +++ b/komga-webui/src/services/komga-readlists.service.ts @@ -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 { + 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) + } + } } diff --git a/komga-webui/src/services/komga-sse.service.ts b/komga-webui/src/services/komga-sse.service.ts index d287a49fd..c020dbeeb 100644 --- a/komga-webui/src/services/komga-sse.service.ts +++ b/komga-webui/src/services/komga-sse.service.ts @@ -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)) } diff --git a/komga-webui/src/types/events.ts b/komga-webui/src/types/events.ts index cbb9a9c49..07fc29fda 100644 --- a/komga-webui/src/types/events.ts +++ b/komga-webui/src/types/events.ts @@ -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' diff --git a/komga-webui/src/types/komga-books.ts b/komga-webui/src/types/komga-books.ts index a3d01300f..628750d9b 100644 --- a/komga-webui/src/types/komga-books.ts +++ b/komga-webui/src/types/komga-books.ts @@ -134,3 +134,10 @@ export interface BookImportDto { upgradeBookId?: string, destinationName?: string, } + +export interface BookThumbnailDto { + id: string, + bookId: string, + type: string, + selected: boolean +} diff --git a/komga-webui/src/types/komga-collections.ts b/komga-webui/src/types/komga-collections.ts index 0e47826fc..5b1cb2d79 100644 --- a/komga-webui/src/types/komga-collections.ts +++ b/komga-webui/src/types/komga-collections.ts @@ -19,3 +19,10 @@ interface CollectionUpdateDto { ordered?: boolean, seriesIds?: string[] } + +interface CollectionThumbnailDto { + id: string, + collectionId: string, + type: string, + selected: boolean +} diff --git a/komga-webui/src/types/komga-readlists.ts b/komga-webui/src/types/komga-readlists.ts index 47a47a2d0..847d35cea 100644 --- a/komga-webui/src/types/komga-readlists.ts +++ b/komga-webui/src/types/komga-readlists.ts @@ -36,3 +36,10 @@ interface ReadListRequestBookDto { series: string, number: string, } + +interface ReadListThumbnailDto { + id: string, + readListId: string, + type: string, + selected: boolean +} diff --git a/komga-webui/src/types/komga-sse.ts b/komga-webui/src/types/komga-sse.ts index 1a58a8f9d..2d08649cb 100644 --- a/komga-webui/src/types/komga-sse.ts +++ b/komga-webui/src/types/komga-sse.ts @@ -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 { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt index 0ebd02bfc..45f75fbc1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/BookLifecycle.kt @@ -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) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt index c6f1a6ceb..163501091 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/ReadListLifecycle.kt @@ -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) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt index 94756bceb..89df88953 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesCollectionLifecycle.kt @@ -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) { diff --git a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt index 6034a1b9a..b01ab075a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt +++ b/komga/src/main/kotlin/org/gotson/komga/domain/service/SeriesLifecycle.kt @@ -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) { diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt index 2b54dedc8..d584391d2 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/BookController.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt index 344e637b9..45c28fc6a 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/ReadListController.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt index fccfa3d43..09943e871 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesCollectionController.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt index 157cca1b4..553069384 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/api/rest/SeriesController.kt @@ -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) } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt index 6110aa4e4..4e0296b57 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/SseController.kt @@ -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)) } } diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt index f0978ffaa..ba0a20715 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailBookSseDto.kt @@ -3,4 +3,5 @@ package org.gotson.komga.interfaces.sse.dto data class ThumbnailBookSseDto( val bookId: String, val seriesId: String, + val selected: Boolean, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailReadListSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailReadListSseDto.kt index 5a87ef662..83f0b8921 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailReadListSseDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailReadListSseDto.kt @@ -2,4 +2,5 @@ package org.gotson.komga.interfaces.sse.dto data class ThumbnailReadListSseDto( val readListId: String, + val selected: Boolean, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesCollectionSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesCollectionSseDto.kt index 52f5725ef..74e111ce1 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesCollectionSseDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesCollectionSseDto.kt @@ -2,4 +2,5 @@ package org.gotson.komga.interfaces.sse.dto data class ThumbnailSeriesCollectionSseDto( val collectionId: String, + val selected: Boolean, ) diff --git a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt index 5a2a43438..aca2c7e1e 100644 --- a/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt +++ b/komga/src/main/kotlin/org/gotson/komga/interfaces/sse/dto/ThumbnailSeriesSseDto.kt @@ -2,4 +2,5 @@ package org.gotson.komga.interfaces.sse.dto data class ThumbnailSeriesSseDto( val seriesId: String, + val selected: Boolean, )