diff --git a/komga-webui/src/components/Dialogs.vue b/komga-webui/src/components/Dialogs.vue index a0840746b..38014566e 100644 --- a/komga-webui/src/components/Dialogs.vue +++ b/komga-webui/src/components/Dialogs.vue @@ -19,6 +19,25 @@ @deleted="collectionDeleted" /> + + + + + + { + this.$eventHub.$emit(BOOK_CHANGED, bookToEventBookChanged(b)) + }) + } else { + this.$eventHub.$emit(BOOK_CHANGED, bookToEventBookChanged(this.addToReadListBooks)) + } + this.$eventHub.$emit(READLIST_CHANGED, readListToEventReadListChanged(readList)) + }, + readListUpdated () { + this.$eventHub.$emit(READLIST_CHANGED, readListToEventReadListChanged(this.editReadList)) + }, + readListDeleted () { + this.$eventHub.$emit(READLIST_DELETED, readListToEventReadListDeleted(this.deleteReadList)) + }, libraryDeleted () { this.$eventHub.$emit(LIBRARY_DELETED, libraryToEventLibraryDeleted(this.deleteLibrary)) }, diff --git a/komga-webui/src/components/ItemCard.vue b/komga-webui/src/components/ItemCard.vue index 0c212ff1c..af3e85f4c 100644 --- a/komga-webui/src/components/ItemCard.vue +++ b/komga-webui/src/components/ItemCard.vue @@ -80,6 +80,10 @@ :collection="item" :menu.sync="actionMenuState" /> + @@ -119,13 +123,14 @@ import { ReadStatus } from '@/types/enum-books' import { createItem, Item, ItemTypes } from '@/types/items' import Vue from 'vue' import { RawLocation } from 'vue-router' +import ReadListActionsMenu from '@/components/menus/ReadListActionsMenu.vue' export default Vue.extend({ name: 'ItemCard', - components: { BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu }, + components: { BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu, ReadListActionsMenu }, props: { item: { - type: Object as () => BookDto | SeriesDto | CollectionDto, + type: Object as () => BookDto | SeriesDto | CollectionDto | ReadListDto, required: true, }, // hide the bottom part of the card @@ -184,7 +189,7 @@ export default Vue.extend({ overlay (): boolean { return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages || this.actionMenu }, - computedItem (): Item { + computedItem (): Item { return createItem(this.item) }, disableHover (): boolean { diff --git a/komga-webui/src/components/LibraryNavigation.vue b/komga-webui/src/components/LibraryNavigation.vue index f1ed9b3a5..30bc66f96 100644 --- a/komga-webui/src/components/LibraryNavigation.vue +++ b/komga-webui/src/components/LibraryNavigation.vue @@ -1,31 +1,78 @@ diff --git a/komga-webui/src/components/ReadListsExpansionPanels.vue b/komga-webui/src/components/ReadListsExpansionPanels.vue new file mode 100644 index 000000000..00ff9adeb --- /dev/null +++ b/komga-webui/src/components/ReadListsExpansionPanels.vue @@ -0,0 +1,71 @@ + + + diff --git a/komga-webui/src/components/SearchBox.vue b/komga-webui/src/components/SearchBox.vue index bd17fa7b5..cdf66d79d 100644 --- a/komga-webui/src/components/SearchBox.vue +++ b/komga-webui/src/components/SearchBox.vue @@ -19,7 +19,9 @@ :min-width="$vuetify.breakpoint.mdAndUp ? $vuetify.breakpoint.width * .4 : $vuetify.breakpoint.width * .8" > - No results + + No results + + diff --git a/komga-webui/src/components/bars/BooksMultiSelectBar.vue b/komga-webui/src/components/bars/BooksMultiSelectBar.vue index 59cdbbbeb..bf063c240 100644 --- a/komga-webui/src/components/bars/BooksMultiSelectBar.vue +++ b/komga-webui/src/components/bars/BooksMultiSelectBar.vue @@ -28,6 +28,15 @@ + + + + Add to read list + + + - + Analyze Refresh metadata + + Add to read list + Mark as read @@ -69,6 +72,9 @@ export default Vue.extend({ refreshMetadata () { this.$komgaBooks.refreshMetadata(this.book) }, + addToReadList () { + this.$store.dispatch('dialogAddBooksToReadList', this.book) + }, async markRead () { const readProgress = { completed: true } as ReadProgressUpdateDto await this.$komgaBooks.updateReadProgress(this.book.id, readProgress) diff --git a/komga-webui/src/components/menus/CollectionActionsMenu.vue b/komga-webui/src/components/menus/CollectionActionsMenu.vue index 04fa224e1..2809c5347 100644 --- a/komga-webui/src/components/menus/CollectionActionsMenu.vue +++ b/komga-webui/src/components/menus/CollectionActionsMenu.vue @@ -6,7 +6,7 @@ mdi-dots-vertical - + Delete diff --git a/komga-webui/src/components/menus/LibraryActionsMenu.vue b/komga-webui/src/components/menus/LibraryActionsMenu.vue index 43bb828d5..cc5df51c7 100644 --- a/komga-webui/src/components/menus/LibraryActionsMenu.vue +++ b/komga-webui/src/components/menus/LibraryActionsMenu.vue @@ -6,7 +6,7 @@ mdi-dots-vertical - + Scan library files diff --git a/komga-webui/src/components/menus/ReadListActionsMenu.vue b/komga-webui/src/components/menus/ReadListActionsMenu.vue new file mode 100644 index 000000000..e859940ce --- /dev/null +++ b/komga-webui/src/components/menus/ReadListActionsMenu.vue @@ -0,0 +1,57 @@ + + + diff --git a/komga-webui/src/components/menus/SeriesActionsMenu.vue b/komga-webui/src/components/menus/SeriesActionsMenu.vue index 88e09e166..20d3a957e 100644 --- a/komga-webui/src/components/menus/SeriesActionsMenu.vue +++ b/komga-webui/src/components/menus/SeriesActionsMenu.vue @@ -6,7 +6,7 @@ mdi-dots-vertical - + Analyze diff --git a/komga-webui/src/functions/urls.ts b/komga-webui/src/functions/urls.ts index 840dc7d8e..2788f4fab 100644 --- a/komga-webui/src/functions/urls.ts +++ b/komga-webui/src/functions/urls.ts @@ -39,3 +39,7 @@ export function seriesThumbnailUrl (seriesId: string): string { export function collectionThumbnailUrl (collectionId: string): string { return `${urls.originNoSlash}/api/v1/collections/${collectionId}/thumbnail` } + +export function readListThumbnailUrl (readListId: string): string { + return `${urls.originNoSlash}/api/v1/readlists/${readListId}/thumbnail` +} diff --git a/komga-webui/src/main.ts b/komga-webui/src/main.ts index 305b6a1bb..50aac7f19 100644 --- a/komga-webui/src/main.ts +++ b/komga-webui/src/main.ts @@ -11,6 +11,7 @@ import httpPlugin from './plugins/http.plugin' import komgaBooks from './plugins/komga-books.plugin' import komgaClaim from './plugins/komga-claim.plugin' import komgaCollections from './plugins/komga-collections.plugin' +import komgaReadLists from './plugins/komga-readlists.plugin' import komgaFileSystem from './plugins/komga-filesystem.plugin' import komgaLibraries from './plugins/komga-libraries.plugin' import komgaReferential from './plugins/komga-referential.plugin' @@ -30,6 +31,7 @@ Vue.use(httpPlugin) Vue.use(komgaFileSystem, { http: Vue.prototype.$http }) Vue.use(komgaSeries, { http: Vue.prototype.$http }) Vue.use(komgaCollections, { http: Vue.prototype.$http }) +Vue.use(komgaReadLists, { http: Vue.prototype.$http }) Vue.use(komgaBooks, { http: Vue.prototype.$http }) Vue.use(komgaReferential, { http: Vue.prototype.$http }) Vue.use(komgaClaim, { http: Vue.prototype.$http }) diff --git a/komga-webui/src/plugins/komga-readlists.plugin.ts b/komga-webui/src/plugins/komga-readlists.plugin.ts new file mode 100644 index 000000000..9ae4889a8 --- /dev/null +++ b/komga-webui/src/plugins/komga-readlists.plugin.ts @@ -0,0 +1,17 @@ +import { AxiosInstance } from 'axios' +import _Vue from 'vue' +import KomgaReadListsService from '@/services/komga-readlists.service' + +export default { + install ( + Vue: typeof _Vue, + { http }: { http: AxiosInstance }) { + Vue.prototype.$komgaReadLists = new KomgaReadListsService(http) + }, +} + +declare module 'vue/types/vue' { + interface Vue { + $komgaReadLists: KomgaReadListsService; + } +} diff --git a/komga-webui/src/router.ts b/komga-webui/src/router.ts index aa79ba6b9..d53433791 100644 --- a/komga-webui/src/router.ts +++ b/komga-webui/src/router.ts @@ -85,12 +85,25 @@ const router = new Router({ component: () => import(/* webpackChunkName: "browse-collections" */ './views/BrowseCollections.vue'), props: (route) => ({ libraryId: route.params.libraryId }), }, + { + path: '/libraries/:libraryId/readlists', + name: 'browse-readlists', + beforeEnter: noLibraryGuard, + component: () => import(/* webpackChunkName: "browse-readlists" */ './views/BrowseReadLists.vue'), + props: (route) => ({ libraryId: route.params.libraryId }), + }, { path: '/collections/:collectionId', name: 'browse-collection', component: () => import(/* webpackChunkName: "browse-collection" */ './views/BrowseCollection.vue'), props: (route) => ({ collectionId: route.params.collectionId }), }, + { + path: '/readlists/:readListId', + name: 'browse-readlist', + component: () => import(/* webpackChunkName: "browse-readlist" */ './views/BrowseReadList.vue'), + props: (route) => ({ readListId: route.params.readListId }), + }, { path: '/series/:seriesId', name: 'browse-series', diff --git a/komga-webui/src/services/komga-books.service.ts b/komga-webui/src/services/komga-books.service.ts index 2ccc70890..8f9748945 100644 --- a/komga-webui/src/services/komga-books.service.ts +++ b/komga-webui/src/services/komga-books.service.ts @@ -101,6 +101,18 @@ export default class KomgaBooksService { } } + async getReadLists (bookId: string): Promise { + try { + return (await this.http.get(`${API_BOOKS}/${bookId}/readlists`)).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve read lists' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + async analyzeBook (book: BookDto) { try { await this.http.post(`${API_BOOKS}/${book.id}/analyze`) diff --git a/komga-webui/src/services/komga-readlists.service.ts b/komga-webui/src/services/komga-readlists.service.ts new file mode 100644 index 000000000..56977e91f --- /dev/null +++ b/komga-webui/src/services/komga-readlists.service.ts @@ -0,0 +1,95 @@ +import { AxiosInstance } from 'axios' + +const qs = require('qs') + +const API_READLISTS = '/api/v1/readlists' + +export default class KomgaReadListsService { + private http: AxiosInstance + + constructor (http: AxiosInstance) { + this.http = http + } + + async getReadLists (libraryIds?: string[], pageRequest?: PageRequest, search?: string): Promise> { + try { + const params = { ...pageRequest } as any + if (libraryIds) params.library_id = libraryIds + if (search) params.search = search + + return (await this.http.get(API_READLISTS, { + params: params, + paramsSerializer: params => qs.stringify(params, { indices: false }), + })).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve readLists' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async getOneReadList (readListId: string): Promise { + try { + return (await this.http.get(`${API_READLISTS}/${readListId}`)).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve readList' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async postReadList (readList: ReadListCreationDto): Promise { + try { + return (await this.http.post(API_READLISTS, readList)).data + } catch (e) { + let msg = `An error occurred while trying to add readList '${readList.name}'` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async patchReadList (readListId: string, readList: ReadListUpdateDto) { + try { + await this.http.patch(`${API_READLISTS}/${readListId}`, readList) + } catch (e) { + let msg = `An error occurred while trying to update readList '${readListId}'` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async deleteReadList (readListId: string) { + try { + await this.http.delete(`${API_READLISTS}/${readListId}`) + } catch (e) { + let msg = `An error occurred while trying to delete readList '${readListId}'` + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } + + async getBooks (readListId: string, pageRequest?: PageRequest): Promise> { + try { + const params = { ...pageRequest } + return (await this.http.get(`${API_READLISTS}/${readListId}/books`, { + params: params, + })).data + } catch (e) { + let msg = 'An error occurred while trying to retrieve books' + if (e.response.data.message) { + msg += `: ${e.response.data.message}` + } + throw new Error(msg) + } + } +} diff --git a/komga-webui/src/store.ts b/komga-webui/src/store.ts index 251dd7e45..8d1724bf1 100644 --- a/komga-webui/src/store.ts +++ b/komga-webui/src/store.ts @@ -5,22 +5,34 @@ Vue.use(Vuex) export default new Vuex.Store({ state: { + // collections addToCollectionSeries: {} as SeriesDto | SeriesDto[], addToCollectionDialog: false, editCollection: {} as CollectionDto, editCollectionDialog: false, deleteCollection: {} as CollectionDto, deleteCollectionDialog: false, + // read lists + addToReadListBooks: {} as BookDto | BookDto[], + addToReadListDialog: false, + editReadList: {} as ReadListDto, + editReadListDialog: false, + deleteReadList: {} as ReadListDto, + deleteReadListDialog: false, + // libraries editLibrary: {} as LibraryDto | undefined, editLibraryDialog: false, deleteLibrary: {} as LibraryDto, deleteLibraryDialog: false, + // books updateBooks: {} as BookDto | BookDto[], updateBooksDialog: false, + // series updateSeries: {} as SeriesDto | SeriesDto[], updateSeriesDialog: false, }, mutations: { + // Collections setAddToCollectionSeries (state, series) { state.addToCollectionSeries = series }, @@ -36,27 +48,49 @@ export default new Vuex.Store({ setDeleteCollection (state, collection) { state.deleteCollection = collection }, + setDeleteCollectionDialog (state, dialog) { + state.deleteCollectionDialog = dialog + }, + // Read Lists + setAddToReadListBooks (state, Book) { + state.addToReadListBooks = Book + }, + setAddToReadListDialog (state, dialog) { + state.addToReadListDialog = dialog + }, + setEditReadList (state, ReadList) { + state.editReadList = ReadList + }, + setEditReadListDialog (state, dialog) { + state.editReadListDialog = dialog + }, + setDeleteReadList (state, ReadList) { + state.deleteReadList = ReadList + }, + setDeleteReadListDialog (state, dialog) { + state.deleteReadListDialog = dialog + }, + // Libraries setEditLibrary (state, library) { state.editLibrary = library }, setEditLibraryDialog (state, dialog) { state.editLibraryDialog = dialog }, - setDeleteCollectionDialog (state, dialog) { - state.deleteCollectionDialog = dialog - }, setDeleteLibrary (state, library) { state.deleteLibrary = library }, setDeleteLibraryDialog (state, dialog) { state.deleteLibraryDialog = dialog }, + // Books setUpdateBooks (state, books) { state.updateBooks = books }, setUpdateBooksDialog (state, dialog) { state.updateBooksDialog = dialog }, + // Series setUpdateSeries (state, series) { state.updateSeries = series }, @@ -65,6 +99,7 @@ export default new Vuex.Store({ }, }, actions: { + // collections dialogAddSeriesToCollection ({ commit }, series) { commit('setAddToCollectionSeries', series) commit('setAddToCollectionDialog', true) @@ -86,6 +121,29 @@ export default new Vuex.Store({ dialogDeleteCollectionDisplay ({ commit }, value) { commit('setDeleteCollectionDialog', value) }, + // read lists + dialogAddBooksToReadList ({ commit }, books) { + commit('setAddToReadListBooks', books) + commit('setAddToReadListDialog', true) + }, + dialogAddBooksToReadListDisplay ({ commit }, value) { + commit('setAddToReadListDialog', value) + }, + dialogEditReadList ({ commit }, readList) { + commit('setEditReadList', readList) + commit('setEditReadListDialog', true) + }, + dialogEditReadListDisplay ({ commit }, value) { + commit('setEditReadListDialog', value) + }, + dialogDeleteReadList ({ commit }, readList) { + commit('setDeleteReadList', readList) + commit('setDeleteReadListDialog', true) + }, + dialogDeleteReadListDisplay ({ commit }, value) { + commit('setDeleteReadListDialog', value) + }, + // libraries dialogAddLibrary ({ commit }) { commit('setEditLibrary', undefined) commit('setEditLibraryDialog', true) @@ -104,6 +162,7 @@ export default new Vuex.Store({ dialogDeleteLibraryDisplay ({ commit }, value) { commit('setDeleteLibraryDialog', value) }, + // books dialogUpdateBooks ({ commit }, books) { commit('setUpdateBooks', books) commit('setUpdateBooksDialog', true) @@ -111,6 +170,7 @@ export default new Vuex.Store({ dialogUpdateBooksDisplay ({ commit }, value) { commit('setUpdateBooksDialog', value) }, + // series dialogUpdateSeries ({ commit }, series) { commit('setUpdateSeries', series) commit('setUpdateSeriesDialog', true) diff --git a/komga-webui/src/types/events-payloads.ts b/komga-webui/src/types/events-payloads.ts index 72f937e07..0f8b59f94 100644 --- a/komga-webui/src/types/events-payloads.ts +++ b/komga-webui/src/types/events-payloads.ts @@ -16,6 +16,14 @@ interface EventCollectionDeleted { id: string } +interface EventReadListChanged { + id: string +} + +interface EventReadListDeleted { + id: string +} + interface EventLibraryAdded { id: string } diff --git a/komga-webui/src/types/events.ts b/komga-webui/src/types/events.ts index f8bedb7ca..b1a683459 100644 --- a/komga-webui/src/types/events.ts +++ b/komga-webui/src/types/events.ts @@ -2,6 +2,8 @@ export const BOOK_CHANGED = 'book-changed' export const SERIES_CHANGED = 'series-changed' export const COLLECTION_DELETED = 'collection-deleted' export const COLLECTION_CHANGED = 'collection-changed' +export const READLIST_DELETED = 'readlist-deleted' +export const READLIST_CHANGED = 'readlist-changed' export const LIBRARY_ADDED = 'library-added' export const LIBRARY_CHANGED = 'library-changed' export const LIBRARY_DELETED = 'library-deleted' @@ -32,6 +34,18 @@ export function collectionToEventCollectionDeleted (collection: CollectionDto): } as EventCollectionDeleted } +export function readListToEventReadListChanged (readList: ReadListDto): EventReadListChanged { + return { + id: readList.id, + } as EventReadListChanged +} + +export function readListToEventReadListDeleted (readList: ReadListDto): EventReadListDeleted { + return { + id: readList.id, + } as EventReadListDeleted +} + export function libraryToEventLibraryAdded (library: LibraryDto): EventLibraryAdded { return { id: library.id, diff --git a/komga-webui/src/types/items.ts b/komga-webui/src/types/items.ts index 48d7fe74e..6a86cd1e6 100644 --- a/komga-webui/src/types/items.ts +++ b/komga-webui/src/types/items.ts @@ -1,4 +1,4 @@ -import { bookThumbnailUrl, collectionThumbnailUrl, seriesThumbnailUrl } from '@/functions/urls' +import { bookThumbnailUrl, collectionThumbnailUrl, readListThumbnailUrl, seriesThumbnailUrl } from '@/functions/urls' import { RawLocation } from 'vue-router/types/router' function plural (count: number, singular: string, plural: string) { @@ -6,11 +6,13 @@ function plural (count: number, singular: string, plural: string) { } export enum ItemTypes { - BOOK, SERIES, COLLECTION + BOOK, SERIES, COLLECTION, READLIST } -export function createItem (item: BookDto | SeriesDto | CollectionDto): Item { - if ('seriesIds' in item) { +export function createItem (item: BookDto | SeriesDto | CollectionDto | ReadListDto): Item { + if ('bookIds' in item) { + return new ReadListItem(item) + } else if ('seriesIds' in item) { return new CollectionItem(item) } else if ('seriesId' in item) { return new BookItem(item) @@ -116,3 +118,26 @@ export class CollectionItem extends Item { return { name: 'browse-collection', params: { collectionId: this.item.id.toString() } } } } + +export class ReadListItem extends Item { + thumbnailUrl (): string { + return readListThumbnailUrl(this.item.id) + } + + type (): ItemTypes { + return ItemTypes.READLIST + } + + title (): string { + return this.item.name + } + + body (): string { + const c = this.item.bookIds.length + return `${c} Books` + } + + to (): RawLocation { + return { name: 'browse-readlist', params: { readListId: this.item.id.toString() } } + } +} diff --git a/komga-webui/src/types/komga-libraries.ts b/komga-webui/src/types/komga-libraries.ts index f76223212..708338848 100644 --- a/komga-webui/src/types/komga-libraries.ts +++ b/komga-webui/src/types/komga-libraries.ts @@ -4,6 +4,7 @@ interface LibraryCreationDto { importComicInfoBook: boolean, importComicInfoSeries: boolean, importComicInfoCollection: boolean, + importComicInfoReadList: boolean, importEpubBook: boolean, importEpubSeries: boolean, importLocalArtwork: boolean, @@ -17,6 +18,7 @@ interface LibraryUpdateDto { importComicInfoBook: boolean, importComicInfoSeries: boolean, importComicInfoCollection: boolean, + importComicInfoReadList: boolean, importEpubBook: boolean, importEpubSeries: boolean, importLocalArtwork: boolean, @@ -31,6 +33,7 @@ interface LibraryDto { importComicInfoBook: boolean, importComicInfoSeries: boolean, importComicInfoCollection: boolean, + importComicInfoReadList: boolean, importEpubBook: boolean, importEpubSeries: boolean, importLocalArtwork: boolean, diff --git a/komga-webui/src/types/komga-readlists.ts b/komga-webui/src/types/komga-readlists.ts new file mode 100644 index 000000000..a99cfc915 --- /dev/null +++ b/komga-webui/src/types/komga-readlists.ts @@ -0,0 +1,18 @@ +interface ReadListDto { + id: string, + name: string, + filtered: boolean, + bookIds: string[], + createdDate: string, + lastModifiedDate: string +} + +interface ReadListCreationDto { + name: string, + bookIds: string[] +} + +interface ReadListUpdateDto { + name?: string, + bookIds?: string[] +} diff --git a/komga-webui/src/types/library.ts b/komga-webui/src/types/library.ts new file mode 100644 index 000000000..b413d73bd --- /dev/null +++ b/komga-webui/src/types/library.ts @@ -0,0 +1 @@ +export const LIBRARIES_ALL = 'all' diff --git a/komga-webui/src/views/BrowseBook.vue b/komga-webui/src/views/BrowseBook.vue index 4054b26fe..b51ea1d07 100644 --- a/komga-webui/src/views/BrowseBook.vue +++ b/komga-webui/src/views/BrowseBook.vue @@ -89,8 +89,7 @@ {{ book.metadata.ageRating }}+ - {{ book.metadata.releaseDate | moment - ('MMMM DD, YYYY') }} + {{ book.metadata.releaseDate | moment('MMMM DD, YYYY') }} @@ -121,6 +120,18 @@ + + + + + + + + + + + + @@ -199,10 +210,11 @@ import { bookFileUrl, bookThumbnailUrl } from '@/functions/urls' import { ReadStatus } from '@/types/enum-books' import { BOOK_CHANGED, LIBRARY_DELETED } from '@/types/events' import Vue from 'vue' +import ReadListsExpansionPanels from '@/components/ReadListsExpansionPanels.vue' export default Vue.extend({ name: 'BrowseBook', - components: { ToolbarSticky, Badge, ItemCard, BookActionsMenu }, + components: { ToolbarSticky, Badge, ItemCard, BookActionsMenu, ReadListsExpansionPanels }, data: () => { return { book: {} as BookDto, @@ -210,6 +222,7 @@ export default Vue.extend({ siblings: [] as BookDto[], siblingPrevious: {} as BookDto, siblingNext: {} as BookDto, + readLists: [] as ReadListDto[], } }, async created () { @@ -291,6 +304,7 @@ export default Vue.extend({ this.book = await this.$komgaBooks.getBook(bookId) this.series = await this.$komgaSeries.getOneSeries(this.book.seriesId) this.siblings = (await this.$komgaSeries.getBooks(this.book.seriesId, { unpaged: true } as PageRequest)).content + this.readLists = await this.$komgaBooks.getReadLists(this.bookId) if (this.$_.has(this.book, 'metadata.title')) { document.title = `Komga - ${getBookTitleCompact(this.book.metadata.title, this.series.metadata.title)}` diff --git a/komga-webui/src/views/BrowseCollection.vue b/komga-webui/src/views/BrowseCollection.vue index 6404bef5d..6352f94b7 100644 --- a/komga-webui/src/views/BrowseCollection.vue +++ b/komga-webui/src/views/BrowseCollection.vue @@ -82,6 +82,7 @@ import ToolbarSticky from '@/components/bars/ToolbarSticky.vue' import { COLLECTION_CHANGED, COLLECTION_DELETED, SERIES_CHANGED } from '@/types/events' import Vue from 'vue' import SeriesMultiSelectBar from '@/components/bars/SeriesMultiSelectBar.vue' +import { LIBRARIES_ALL } from '@/types/library' export default Vue.extend({ name: 'BrowseCollection', @@ -205,7 +206,7 @@ export default Vue.extend({ this.$store.dispatch('dialogEditCollection', this.collection) }, afterDelete () { - this.$router.push({ name: 'browse-collections', params: { libraryId: '0' } }) + this.$router.push({ name: 'browse-collections', params: { libraryId: LIBRARIES_ALL } }) }, reloadSeries (event: EventSeriesChanged) { if (this.series.some(s => s.id === event.id)) this.loadCollection(this.collectionId) diff --git a/komga-webui/src/views/BrowseCollections.vue b/komga-webui/src/views/BrowseCollections.vue index 1981653cf..87d072d5c 100644 --- a/komga-webui/src/views/BrowseCollections.vue +++ b/komga-webui/src/views/BrowseCollections.vue @@ -43,12 +43,12 @@ import ItemBrowser from '@/components/ItemBrowser.vue' import LibraryNavigation from '@/components/LibraryNavigation.vue' import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue' import PageSizeSelect from '@/components/PageSizeSelect.vue' -import { COLLECTION_CHANGED, LIBRARY_CHANGED } from '@/types/events' +import { COLLECTION_CHANGED, COLLECTION_DELETED, LIBRARY_CHANGED } from '@/types/events' import Vue from 'vue' import { Location } from 'vue-router' +import { LIBRARIES_ALL } from '@/types/library' const cookiePageSize = 'pagesize' -const all = 'all' export default Vue.extend({ name: 'BrowseCollections', @@ -75,15 +75,17 @@ export default Vue.extend({ props: { libraryId: { type: String, - default: all, + default: LIBRARIES_ALL, }, }, created () { this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCollections) + this.$eventHub.$on(COLLECTION_DELETED, this.reloadCollections) this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibrary) }, beforeDestroy () { this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCollections) + this.$eventHub.$off(COLLECTION_DELETED, this.reloadCollections) this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary) }, mounted () { @@ -189,7 +191,7 @@ export default Vue.extend({ size: this.pageSize, } as PageRequest - const lib = libraryId !== all ? [libraryId] : undefined + const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined const collectionsPage = await this.$komgaCollections.getCollections(lib, pageRequest) this.totalPages = collectionsPage.totalPages @@ -197,7 +199,7 @@ export default Vue.extend({ this.collections = collectionsPage.content }, getLibraryLazy (libraryId: string): LibraryDto | undefined { - if (libraryId !== all) { + if (libraryId !== LIBRARIES_ALL) { return this.$store.getters.getLibraryById(libraryId) } else { return undefined diff --git a/komga-webui/src/views/BrowseLibraries.vue b/komga-webui/src/views/BrowseLibraries.vue index dc31170e9..135a14d3f 100644 --- a/komga-webui/src/views/BrowseLibraries.vue +++ b/komga-webui/src/views/BrowseLibraries.vue @@ -35,9 +35,7 @@ @edit="editMultipleSeries" /> - + +
+ + + + + + {{ readList.name }} + {{ readList.bookIds.length }} + + + + + + + + Edit elements + + + + + + + Edit read list + + + + + + + + + + + + mdi-close + + + + mdi-check + + + + + + + + + + + +
+ + + diff --git a/komga-webui/src/views/BrowseReadLists.vue b/komga-webui/src/views/BrowseReadLists.vue new file mode 100644 index 000000000..55a72fb72 --- /dev/null +++ b/komga-webui/src/views/BrowseReadLists.vue @@ -0,0 +1,213 @@ + + + diff --git a/komga-webui/src/views/BrowseSeries.vue b/komga-webui/src/views/BrowseSeries.vue index e53212f28..e011824ad 100644 --- a/komga-webui/src/views/BrowseSeries.vue +++ b/komga-webui/src/views/BrowseSeries.vue @@ -44,6 +44,7 @@ @unselect-all="selectedBooks = []" @mark-read="markSelectedRead" @mark-unread="markSelectedUnread" + @add-to-readlist="addToReadList" @edit="editMultipleBooks" /> @@ -133,7 +134,7 @@ import SortMenuButton from '@/components/SortMenuButton.vue' import { parseQueryFilter, parseQuerySort } from '@/functions/query-params' import { seriesThumbnailUrl } from '@/functions/urls' import { ReadStatus } from '@/types/enum-books' -import { BOOK_CHANGED, LIBRARY_DELETED, SERIES_CHANGED } from '@/types/events' +import { BOOK_CHANGED, LIBRARY_DELETED, READLIST_CHANGED, SERIES_CHANGED } from '@/types/events' import Vue from 'vue' import { Location } from 'vue-router' @@ -218,11 +219,13 @@ export default Vue.extend({ }, created () { this.$eventHub.$on(SERIES_CHANGED, this.reloadSeries) + this.$eventHub.$on(READLIST_CHANGED, this.reloadSeries) this.$eventHub.$on(BOOK_CHANGED, this.reloadBooks) this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted) }, beforeDestroy () { this.$eventHub.$off(SERIES_CHANGED, this.reloadSeries) + this.$eventHub.$off(READLIST_CHANGED, this.reloadSeries) this.$eventHub.$off(BOOK_CHANGED, this.reloadBooks) this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted) }, @@ -357,6 +360,9 @@ export default Vue.extend({ editMultipleBooks () { this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) }, + addToReadList () { + this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks) + }, async markSelectedRead () { await Promise.all(this.selectedBooks.map(b => this.$komgaBooks.updateReadProgress(b.id, { completed: true }), diff --git a/komga-webui/src/views/Dashboard.vue b/komga-webui/src/views/Dashboard.vue index 69c4528d5..9a36defb4 100644 --- a/komga-webui/src/views/Dashboard.vue +++ b/komga-webui/src/views/Dashboard.vue @@ -14,6 +14,7 @@ @unselect-all="selectedBooks = []" @mark-read="markSelectedBooksRead" @mark-unread="markSelectedBooksUnread" + @add-to-readlist="addToReadList" @edit="editMultipleBooks" /> @@ -259,6 +260,9 @@ export default Vue.extend({ editMultipleBooks () { this.$store.dispatch('dialogUpdateBooks', this.selectedBooks) }, + addToReadList () { + this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks) + }, async markSelectedBooksRead () { await Promise.all(this.selectedBooks.map(b => this.$komgaBooks.updateReadProgress(b.id, { completed: true }), diff --git a/komga-webui/src/views/Search.vue b/komga-webui/src/views/Search.vue index 578754c24..689b4d43f 100644 --- a/komga-webui/src/views/Search.vue +++ b/komga-webui/src/views/Search.vue @@ -20,6 +20,7 @@ @unselect-all="selectedBooks = []" @mark-read="markSelectedBooksRead" @mark-unread="markSelectedBooksUnread" + @add-to-readlist="addToReadList" @edit="editMultipleBooks" /> @@ -79,6 +80,20 @@ + + + + +
@@ -92,7 +107,15 @@ import ToolbarSticky from '@/components/bars/ToolbarSticky.vue' import EmptyState from '@/components/EmptyState.vue' import HorizontalScroller from '@/components/HorizontalScroller.vue' import ItemBrowser from '@/components/ItemBrowser.vue' -import { BOOK_CHANGED, COLLECTION_CHANGED, LIBRARY_DELETED, SERIES_CHANGED } from '@/types/events' +import { + BOOK_CHANGED, + COLLECTION_CHANGED, + COLLECTION_DELETED, + LIBRARY_DELETED, + READLIST_CHANGED, + READLIST_DELETED, + SERIES_CHANGED, +} from '@/types/events' import Vue from 'vue' export default Vue.extend({ @@ -110,6 +133,7 @@ export default Vue.extend({ series: [] as SeriesDto[], books: [] as BookDto[], collections: [] as CollectionDto[], + readLists: [] as ReadListDto[], pageSize: 50, loading: false, selectedSeries: [] as SeriesDto[], @@ -121,12 +145,18 @@ export default Vue.extend({ this.$eventHub.$on(SERIES_CHANGED, this.reloadResults) this.$eventHub.$on(BOOK_CHANGED, this.reloadResults) this.$eventHub.$on(COLLECTION_CHANGED, this.reloadResults) + this.$eventHub.$on(COLLECTION_DELETED, this.reloadResults) + this.$eventHub.$on(READLIST_CHANGED, this.reloadResults) + this.$eventHub.$on(READLIST_DELETED, this.reloadResults) }, beforeDestroy () { this.$eventHub.$off(LIBRARY_DELETED, this.reloadResults) this.$eventHub.$off(SERIES_CHANGED, this.reloadResults) this.$eventHub.$off(BOOK_CHANGED, this.reloadResults) this.$eventHub.$off(COLLECTION_CHANGED, this.reloadResults) + this.$eventHub.$off(COLLECTION_DELETED, this.reloadResults) + this.$eventHub.$off(READLIST_CHANGED, this.reloadResults) + this.$eventHub.$off(READLIST_DELETED, this.reloadResults) }, watch: { '$route.query.q': { @@ -163,7 +193,7 @@ export default Vue.extend({ return this.selectedSeries.length === 0 && this.selectedBooks.length === 0 }, emptyResults (): boolean { - return !this.loading && this.series.length === 0 && this.books.length === 0 && this.collections.length === 0 + return !this.loading && this.series.length === 0 && this.books.length === 0 && this.collections.length === 0 && this.readLists.length === 0 }, }, methods: { @@ -176,6 +206,9 @@ export default Vue.extend({ singleEditCollection (collection: CollectionDto) { this.$store.dispatch('dialogEditCollection', collection) }, + singleEditReadList (readList: ReadListDto) { + this.$store.dispatch('dialogEditReadList', readList) + }, async markSelectedSeriesRead () { await Promise.all(this.selectedSeries.map(s => this.$komgaSeries.markAsRead(s.id), @@ -195,6 +228,9 @@ export default Vue.extend({ addToCollection () { this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries) }, + addToReadList () { + this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks) + }, editMultipleSeries () { this.$store.dispatch('dialogUpdateSeries', this.selectedSeries) }, @@ -227,12 +263,14 @@ export default Vue.extend({ this.series = (await this.$komgaSeries.getSeries(undefined, { size: this.pageSize }, search)).content this.books = (await this.$komgaBooks.getBooks(undefined, { size: this.pageSize }, search)).content this.collections = (await this.$komgaCollections.getCollections(undefined, { size: this.pageSize }, search)).content + this.readLists = (await this.$komgaReadLists.getReadLists(undefined, { size: this.pageSize }, search)).content this.loading = false } else { this.series = [] this.books = [] this.collections = [] + this.readLists = [] } }, },