feat(webui): the UI is now dynamic to events from the server

closes #124
This commit is contained in:
Gauthier Roebroeck 2021-06-21 14:53:06 +08:00
parent 691c7f0071
commit a707fd3594
37 changed files with 741 additions and 330 deletions

View file

@ -6,6 +6,8 @@
<script lang="ts">
import Vue from 'vue'
import {Theme} from "@/types/themes";
import {LIBRARY_ADDED, LIBRARY_CHANGED, LIBRARY_DELETED} from "@/types/events";
import {LibrarySseDto} from "@/types/komga-sse";
const cookieLocale = 'locale'
const cookieTheme = 'theme'
@ -73,9 +75,18 @@ export default Vue.extend({
this.$cookies.keys()
.filter(x => x.startsWith('collection.filter') || x.startsWith('library.filter') || x.startsWith('library.sort'))
.forEach(x => this.$cookies.remove(x))
this.$eventHub.$on(LIBRARY_ADDED, this.reloadLibraries)
this.$eventHub.$on(LIBRARY_DELETED, this.reloadLibraries)
this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibraries)
},
beforeDestroy() {
window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', this.systemThemeChange)
this.$eventHub.$off(LIBRARY_ADDED, this.reloadLibraries)
this.$eventHub.$off(LIBRARY_DELETED, this.reloadLibraries)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibraries)
},
watch: {
"$store.state.persistedState.locale": {
@ -117,6 +128,9 @@ export default Vue.extend({
break
}
},
reloadLibraries(event: LibrarySseDto) {
this.$store.dispatch('getLibraries')
},
},
})
</script>

View file

@ -3,39 +3,31 @@
<collection-add-to-dialog
v-model="addToCollectionDialog"
:series="addToCollectionSeries"
@added="collectionAdded"
@created="collectionAdded"
/>
<collection-edit-dialog
v-model="editCollectionDialog"
:collection="editCollection"
@updated="collectionUpdated"
/>
<collection-delete-dialog
v-model="deleteCollectionDialog"
:collection="deleteCollection"
@deleted="collectionDeleted"
/>
<read-list-add-to-dialog
v-model="addToReadListDialog"
:books="addToReadListBooks"
@added="readListAdded"
@created="readListAdded"
/>
<read-list-edit-dialog
v-model="editReadListDialog"
:read-list="editReadList"
@updated="readListUpdated"
/>
<read-list-delete-dialog
v-model="deleteReadListDialog"
:read-list="deleteReadList"
@deleted="readListDeleted"
/>
<library-edit-dialog
@ -46,19 +38,16 @@
<library-delete-dialog
v-model="deleteLibraryDialog"
:library="deleteLibrary"
@deleted="libraryDeleted"
/>
<edit-books-dialog
v-model="updateBooksDialog"
:books="updateBooks"
@updated="bookUpdated"
/>
<edit-series-dialog
v-model="updateSeriesDialog"
:series="updateSeries"
@updated="seriesUpdated"
/>
</div>
@ -72,27 +61,11 @@ import EditBooksDialog from '@/components/dialogs/EditBooksDialog.vue'
import EditSeriesDialog from '@/components/dialogs/EditSeriesDialog.vue'
import LibraryDeleteDialog from '@/components/dialogs/LibraryDeleteDialog.vue'
import LibraryEditDialog from '@/components/dialogs/LibraryEditDialog.vue'
import {
BOOK_CHANGED,
bookToEventBookChanged,
COLLECTION_CHANGED,
COLLECTION_DELETED,
collectionToEventCollectionChanged,
collectionToEventCollectionDeleted,
LIBRARY_DELETED,
libraryToEventLibraryDeleted,
READLIST_CHANGED,
READLIST_DELETED,
readListToEventReadListChanged,
readListToEventReadListDeleted,
SERIES_CHANGED,
seriesToEventSeriesChanged,
} from '@/types/events'
import Vue from 'vue'
import ReadListAddToDialog from '@/components/dialogs/ReadListAddToDialog.vue'
import ReadListDeleteDialog from '@/components/dialogs/ReadListDeleteDialog.vue'
import ReadListEditDialog from '@/components/dialogs/ReadListEditDialog.vue'
import { BookDto } from '@/types/komga-books'
import {BookDto} from '@/types/komga-books'
import {SeriesDto} from "@/types/komga-series";
export default Vue.extend({
@ -226,49 +199,6 @@ export default Vue.extend({
return this.$store.state.updateSeries
},
},
methods: {
collectionAdded (collection: CollectionDto) {
if (Array.isArray(this.addToCollectionSeries)) {
this.addToCollectionSeries.forEach(s => {
this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(s))
})
} else {
this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.addToCollectionSeries))
}
this.$eventHub.$emit(COLLECTION_CHANGED, collectionToEventCollectionChanged(collection))
},
collectionUpdated () {
this.$eventHub.$emit(COLLECTION_CHANGED, collectionToEventCollectionChanged(this.editCollection))
},
collectionDeleted () {
this.$eventHub.$emit(COLLECTION_DELETED, collectionToEventCollectionDeleted(this.deleteCollection))
},
readListAdded (readList: ReadListDto) {
if (Array.isArray(this.addToReadListBooks)) {
this.addToReadListBooks.forEach(b => {
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))
},
bookUpdated (book: BookDto) {
this.$eventHub.$emit(BOOK_CHANGED, bookToEventBookChanged(book))
},
seriesUpdated (series: SeriesDto) {
this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(series))
},
},
})
</script>

View file

@ -13,6 +13,7 @@
lazy-src="../assets/cover.svg"
aspect-ratio="0.7071"
contain
@error="thumbnailError = true"
>
<!-- unread tick for book -->
<div class="unread" v-if="isUnread"/>
@ -39,7 +40,8 @@
:style="'position: absolute; top: 5px; ' + ($vuetify.rtl ? 'right' : 'left') + ': 10px'"
@click.stop="selectItem"
>
{{ selected || (preselect && hover) ? 'mdi-checkbox-marked-circle' : 'mdi-checkbox-blank-circle-outline'
{{
selected || (preselect && hover) ? 'mdi-checkbox-marked-circle' : 'mdi-checkbox-blank-circle-outline'
}}
</v-icon>
@ -126,10 +128,12 @@ 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";
export default Vue.extend({
name: 'ItemCard',
components: { BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu, ReadListActionsMenu },
components: {BookActionsMenu, SeriesActionsMenu, CollectionActionsMenu, ReadListActionsMenu},
props: {
item: {
type: Object as () => BookDto | SeriesDto | CollectionDto | ReadListDto,
@ -182,79 +186,101 @@ export default Vue.extend({
return {
ItemTypes,
actionMenuState: false,
thumbnailError: false,
thumbnailCacheBust: '',
}
},
created() {
this.$eventHub.$on(THUMBNAILBOOK_ADDED, this.thumbnailBookAdded)
this.$eventHub.$on(THUMBNAILSERIES_ADDED, this.thumbnailSeriesAdded)
},
beforeDestroy() {
this.$eventHub.$off(THUMBNAILBOOK_ADDED, this.thumbnailBookAdded)
this.$eventHub.$off(THUMBNAILSERIES_ADDED, this.thumbnailSeriesAdded)
},
computed: {
canReadPages (): boolean {
canReadPages(): boolean {
return this.$store.getters.mePageStreaming && this.computedItem.type() === ItemTypes.BOOK
},
overlay (): boolean {
overlay(): boolean {
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages || this.actionMenu
},
computedItem (): Item<BookDto | SeriesDto | CollectionDto | ReadListDto> {
computedItem(): Item<BookDto | SeriesDto | CollectionDto | ReadListDto> {
return createItem(this.item)
},
disableHover (): boolean {
disableHover(): boolean {
return !this.overlay
},
thumbnailUrl (): string {
return this.computedItem.thumbnailUrl()
thumbnailUrl(): string {
return this.computedItem.thumbnailUrl() + this.thumbnailCacheBust
},
title (): string {
title(): string {
return this.computedItem.title()
},
subtitleProps (): Object {
subtitleProps(): Object {
return this.computedItem.subtitleProps()
},
body (): string {
body(): string {
return this.computedItem.body()
},
isInProgress (): boolean {
isInProgress(): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.IN_PROGRESS
return false
},
isUnread (): boolean {
isUnread(): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgress(this.item as BookDto) === ReadStatus.UNREAD
return false
},
unreadCount (): number | undefined {
unreadCount(): number | undefined {
if (this.computedItem.type() === ItemTypes.SERIES) return (this.item as SeriesDto).booksUnreadCount + (this.item as SeriesDto).booksInProgressCount
return undefined
},
readProgressPercentage (): number {
readProgressPercentage(): number {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgressPercentage(this.item as BookDto)
return 0
},
bookReady (): boolean {
bookReady(): boolean {
if (this.computedItem.type() === ItemTypes.BOOK) {
return (this.item as BookDto).media.status === 'READY'
}
return false
},
to (): RawLocation {
to(): RawLocation {
return this.computedItem.to()
},
fabTo (): RawLocation {
fabTo(): RawLocation {
return this.computedItem.fabTo()
},
},
methods: {
onClick () {
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))
) {
this.thumbnailCacheBust = '?' + this.$_.random(1000)
}
},
thumbnailSeriesAdded(event: ThumbnailSeriesSseDto) {
if (this.thumbnailError && (this.computedItem.type() === ItemTypes.SERIES && event.seriesId === this.item.id)) {
this.thumbnailCacheBust = '?' + this.$_.random(1000)
}
},
onClick() {
if (this.preselect && this.onSelected !== undefined) {
this.selectItem()
} else if (!this.noLink) {
this.goto()
}
},
goto () {
goto() {
this.$router.push(this.computedItem.to())
},
selectItem () {
selectItem() {
if (this.onSelected !== undefined) {
this.onSelected()
}
},
editItem () {
editItem() {
if (this.onEdit !== undefined) {
this.onEdit(this.item)
}

View file

@ -77,7 +77,7 @@
<script lang="ts">
import Vue from 'vue'
import {COLLECTION_CHANGED, READLIST_CHANGED} from '@/types/events'
import {COLLECTION_ADDED, COLLECTION_DELETED, READLIST_ADDED, READLIST_DELETED} from '@/types/events'
import {LIBRARIES_ALL} from '@/types/library'
export default Vue.extend({
@ -101,18 +101,23 @@ export default Vue.extend({
watch: {
libraryId: {
handler(val) {
this.loadCounts(val)
this.loadReadListCounts(val)
this.loadCollectionCounts(val)
},
immediate: true,
},
},
created() {
this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCounts)
this.$eventHub.$on(READLIST_CHANGED, this.reloadCounts)
this.$eventHub.$on(COLLECTION_ADDED, this.collectionAdded)
this.$eventHub.$on(COLLECTION_DELETED, this.collectionDeleted)
this.$eventHub.$on(READLIST_ADDED, this.readListAdded)
this.$eventHub.$on(READLIST_DELETED, this.readListDeleted)
},
beforeDestroy() {
this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCounts)
this.$eventHub.$off(READLIST_CHANGED, this.reloadCounts)
this.$eventHub.$off(COLLECTION_ADDED, this.collectionAdded)
this.$eventHub.$off(COLLECTION_DELETED, this.collectionDeleted)
this.$eventHub.$off(READLIST_ADDED, this.readListAdded)
this.$eventHub.$off(READLIST_DELETED, this.readListDeleted)
},
computed: {
showRecommended(): boolean {
@ -123,15 +128,27 @@ export default Vue.extend({
},
},
methods: {
reloadCounts() {
this.loadCounts(this.libraryId)
readListAdded() {
if(this.readListsCount === 0) this.loadReadListCounts(this.libraryId)
},
async loadCounts(libraryId: string) {
readListDeleted() {
if(this.readListsCount === 1) this.loadReadListCounts(this.libraryId)
},
collectionAdded() {
if(this.collectionsCount === 0) this.loadCollectionCounts(this.libraryId)
},
collectionDeleted() {
if(this.collectionsCount === 1) this.loadCollectionCounts(this.libraryId)
},
async loadCollectionCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
this.$komgaCollections.getCollections(lib, {size: 0})
.then(v => this.collectionsCount = v.totalElements)
},
async loadReadListCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
await this.$komgaReadLists.getReadLists(lib, {size: 0})
.then(v => this.readListsCount = v.totalElements)
.then(v => this.readListsCount = v.totalElements)
},
},
})

View file

@ -0,0 +1,117 @@
<template>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
bottom
multi-line
vertical
:timeout="snackbar.timeout"
>
<p>{{ snackbar.text }}</p>
<p>{{ snackbar.text2 }}</p>
<template v-slot:action="{ attrs }">
<v-btn
v-if="snackbar.goTo"
color="secondary"
text
v-bind="attrs"
@click="snackbar.goTo.click"
>
{{ snackbar.goTo.text }}
</v-btn>
<v-btn
text
v-bind="attrs"
@click="close"
>{{ $t('common.dismiss') }}</v-btn>
</template>
</v-snackbar>
</template>
<script lang="ts">
import Vue from 'vue'
import {BOOK_IMPORTED} from "@/types/events";
import {convertErrorCodes} from "@/functions/error-codes";
import {BookImportSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'Toaster',
data: function () {
return {
queue: [] as any[],
snackbar: {
show: false,
text: '',
text2: '',
color: undefined,
timeout: 5000,
goTo: {
text: '',
click: () => {
},
},
},
}
},
created() {
this.$eventHub.$on(BOOK_IMPORTED, this.bookImported)
},
beforeDestroy() {
this.$eventHub.$off(BOOK_IMPORTED, this.bookImported)
},
watch: {
'snackbar.show'(val) {
if (!val) {
setTimeout(() => this.next(), 1000)
}
},
queue(val) {
if (val.length > 0) {
this.next()
}
},
},
methods: {
close() {
this.snackbar.show = false
},
next() {
if (this.snackbar.show) {
return
}
if (this.queue.length > 0) {
const snack = this.queue.shift()
this.snackbar.text = snack.text
this.snackbar.text2 = snack.text2
this.snackbar.goTo = snack.goTo
this.snackbar.color = snack.color
this.snackbar.show = true
}
},
async bookImported(event: BookImportSseDto) {
if (event.success && event.bookId) {
const book = await this.$komgaBooks.getBook(event.bookId)
this.queue.push({
text: this.$t('book_import.notification.import_successful', {book: book.metadata.title}).toString(),
text2: this.$t('book_import.notification.source_file', {file: event.sourceFile}).toString(),
goTo: {
text: this.$t('book_import.notification.go_to_book').toString(),
click: () => this.$router.push({name: 'browse-book', params: {bookId: book.id}}),
},
})
} else {
this.queue.push({
text: this.$t('book_import.notification.import_failure', {file: event.sourceFile}).toString(),
text2: convertErrorCodes(event.message || ''),
color: 'error',
})
}
},
},
})
</script>
<style scoped>
</style>

View file

@ -155,7 +155,6 @@ export default Vue.extend({
try {
await this.$komgaCollections.patchCollection(collection.id, toUpdate)
this.$emit('added', collection)
this.dialogClose()
} catch (e) {
this.showSnack(e.message)
@ -170,7 +169,6 @@ export default Vue.extend({
try {
const created = await this.$komgaCollections.postCollection(toCreate)
this.$emit('created', created)
this.dialogClose()
} catch (e) {
this.showSnack(e.message)

View file

@ -95,7 +95,6 @@ export default Vue.extend({
async deleteCollection() {
try {
await this.$komgaCollections.deleteCollection(this.collection.id)
this.$emit('deleted', true)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -138,7 +138,6 @@ export default Vue.extend({
} as CollectionUpdateDto
await this.$komgaCollections.patchCollection(this.collection.id, update)
this.$emit('updated', true)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -532,7 +532,6 @@ export default Vue.extend({
for (const b of toUpdate) {
try {
await this.$komgaBooks.updateMetadata(b.id, metadata)
this.$emit('updated', b)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -591,7 +591,6 @@ export default Vue.extend({
for (const s of toUpdate) {
try {
await this.$komgaSeries.updateMetadata(s.id, metadata)
this.$emit('updated', s)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -94,7 +94,6 @@ export default Vue.extend({
async deleteLibrary() {
try {
await this.$store.dispatch('deleteLibrary', this.library)
this.$emit('deleted', true)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -245,7 +245,6 @@
<script lang="ts">
import FileBrowserDialog from '@/components/dialogs/FileBrowserDialog.vue'
import {LIBRARY_ADDED, LIBRARY_CHANGED, libraryToEventLibraryChanged} from '@/types/events'
import Vue from 'vue'
import {required} from 'vuelidate/lib/validators'
@ -435,10 +434,8 @@ export default Vue.extend({
try {
if (this.library) {
await this.$store.dispatch('updateLibrary', {libraryId: this.library.id, library: library})
this.$eventHub.$emit(LIBRARY_CHANGED, libraryToEventLibraryChanged(this.library))
} else {
await this.$store.dispatch('postLibrary', library)
this.$eventHub.$emit(LIBRARY_ADDED)
}
this.dialogClose()
} catch (e) {

View file

@ -151,7 +151,6 @@ export default Vue.extend({
try {
await this.$komgaReadLists.patchReadList(readList.id, toUpdate)
this.$emit('added', readList)
this.dialogClose()
} catch (e) {
this.showSnack(e.message)
@ -165,7 +164,6 @@ export default Vue.extend({
try {
const created = await this.$komgaReadLists.postReadList(toCreate)
this.$emit('created', created)
this.dialogClose()
} catch (e) {
this.showSnack(e.message)

View file

@ -93,7 +93,6 @@ export default Vue.extend({
async delete() {
try {
await this.$komgaReadLists.deleteReadList(this.readList.id)
this.$emit('deleted', true)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -122,7 +122,6 @@ export default Vue.extend({
} as ReadListUpdateDto
await this.$komgaReadLists.patchReadList(this.readList.id, update)
this.$emit('updated', true)
} catch (e) {
this.showSnack(e.message)
}

View file

@ -29,7 +29,6 @@
<script lang="ts">
import {getReadProgress} from '@/functions/book-progress'
import {ReadStatus} from '@/types/enum-books'
import {BOOK_CHANGED, bookToEventBookChanged} from '@/types/events'
import Vue from 'vue'
import {BookDto, ReadProgressUpdateDto} from '@/types/komga-books'
@ -79,11 +78,9 @@ export default Vue.extend({
async markRead () {
const readProgress = { completed: true } as ReadProgressUpdateDto
await this.$komgaBooks.updateReadProgress(this.book.id, readProgress)
this.$eventHub.$emit(BOOK_CHANGED, bookToEventBookChanged(this.book))
},
async markUnread () {
await this.$komgaBooks.deleteReadProgress(this.book.id)
this.$eventHub.$emit(BOOK_CHANGED, bookToEventBookChanged(this.book))
},
},
})

View file

@ -27,7 +27,6 @@
</div>
</template>
<script lang="ts">
import {SERIES_CHANGED, seriesToEventSeriesChanged} from '@/types/events'
import Vue from 'vue'
import {SeriesDto} from "@/types/komga-series";
@ -76,11 +75,11 @@ export default Vue.extend({
},
async markRead () {
await this.$komgaSeries.markAsRead(this.series.id)
this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
// this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
},
async markUnread () {
await this.$komgaSeries.markAsUnread(this.series.id)
this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
// this.$eventHub.$emit(SERIES_CHANGED, seriesToEventSeriesChanged(this.series))
},
},
})

View file

@ -39,6 +39,12 @@
"field_import_path": "Import from folder",
"info_part1": "This screen lets you import files that are outside your existing libraries. You can only import files into existing Series, in which case Komga will move or copy the files into the directory of the chosen Series.",
"info_part2": "If you choose a number for a book, and a book already exists with that number, then you will be able to compare the 2 books. If you decide to import the book, Komga will upgrade the existing book with the new one, effectively replacing the old file with the new.",
"notification": {
"go_to_book": "Go to book",
"import_failure": "Failed to import book: {file}",
"import_successful": "Book imported successfully: {book}",
"source_file": "Source file: {file}"
},
"row": {
"error_analyze_first": "Book needs to be analyzed first",
"error_choose_series": "Choose a series",
@ -159,6 +165,7 @@
"collections": "Collections",
"create": "Create",
"delete": "Delete",
"dismiss": "Dismiss",
"download": "Download",
"email": "Email",
"filter_no_matches": "The active filter has no matches",
@ -177,12 +184,13 @@
"read": "Read",
"readlists": "Read Lists",
"required": "Required",
"reset_filters": "Reset filters",
"roles": "Roles",
"series": "Series",
"tags": "Tags",
"use_filter_panel_to_change_filter": "Use the filter panel to change the active filter",
"year": "year",
"reset_filters": "Reset filters"
"pending_tasks": "No pending tasks | 1 pending task | {count} pending tasks"
},
"dashboard": {
"keep_reading": "Keep Reading",
@ -469,7 +477,12 @@
"ERR_1014": "No match for book number within series",
"ERR_1015": "Error while deserializing ComicRack ReadingList",
"ERR_1016": "Directory not accessible or not a directory",
"ERR_1017": "Cannot scan folder that is part of an existing library"
"ERR_1017": "Cannot scan folder that is part of an existing library",
"ERR_1018": "File not found",
"ERR_1019": "Cannot import file that is part of an existing library",
"ERR_1020": "Book to upgrade does not belong to provided series",
"ERR_1021": "Destination file already exists",
"ERR_1022": "Newly imported book could not be scanned"
},
"filter": {
"age_rating": "age rating",

View file

@ -18,12 +18,16 @@ import komgaReferential from './plugins/komga-referential.plugin'
import komgaSeries from './plugins/komga-series.plugin'
import komgaUsers from './plugins/komga-users.plugin'
import komgaTransientBooks from './plugins/komga-transientbooks.plugin'
import komgaSse from './plugins/komga-sse.plugin'
import vuetify from './plugins/vuetify'
import './public-path'
import router from './router'
import store from './store'
import i18n from './i18n'
Vue.prototype.$_ = _
Vue.prototype.$eventHub = new Vue()
Vue.use(Vuelidate)
Vue.use(lineClamp)
Vue.use(VueCookies)
@ -39,10 +43,9 @@ Vue.use(komgaClaim, {http: Vue.prototype.$http})
Vue.use(komgaTransientBooks, {http: Vue.prototype.$http})
Vue.use(komgaUsers, {store: store, http: Vue.prototype.$http})
Vue.use(komgaLibraries, {store: store, http: Vue.prototype.$http})
Vue.use(komgaSse, {eventHub: Vue.prototype.$eventHub, store: store})
Vue.use(actuator, {http: Vue.prototype.$http})
Vue.prototype.$_ = _
Vue.prototype.$eventHub = new Vue()
Vue.config.productionTip = false

View file

@ -1,7 +1,7 @@
import KomgaLibrariesService from '@/services/komga-libraries.service'
import { AxiosInstance } from 'axios'
import {AxiosInstance} from 'axios'
import _Vue from 'vue'
import { Module } from 'vuex/types'
import {Module} from 'vuex/types'
let service: KomgaLibrariesService
@ -25,15 +25,12 @@ const vuexModule: Module<any, any> = {
},
async postLibrary ({ dispatch }, library) {
await service.postLibrary(library)
await dispatch('getLibraries')
},
async updateLibrary ({ dispatch }, { libraryId, library }) {
await service.updateLibrary(libraryId, library)
await dispatch('getLibraries')
},
async deleteLibrary ({ dispatch }, library) {
await service.deleteLibrary(library)
await dispatch('getLibraries')
},
},
}

View file

@ -0,0 +1,31 @@
import _Vue from 'vue'
import KomgaSseService from "@/services/komga-sse.service"
import {Module} from "vuex";
const vuexModule: Module<any, any> = {
state: {
taskCount: 0,
},
mutations: {
setTaskCount (state, val) {
state.taskCount = val
},
},
}
export default {
install(
Vue: typeof _Vue,
{eventHub, store}: { eventHub: _Vue, store: any },
) {
store.registerModule('komgaSse', vuexModule)
Vue.prototype.$komgaSse = new KomgaSseService(eventHub, store)
},
}
declare module 'vue/types/vue' {
interface Vue {
$komgaSse: KomgaSseService;
}
}

View file

@ -0,0 +1,99 @@
import urls from '@/functions/urls'
import {
BOOK_ADDED,
BOOK_CHANGED,
BOOK_DELETED,
BOOK_IMPORTED,
COLLECTION_ADDED,
COLLECTION_CHANGED,
COLLECTION_DELETED,
LIBRARY_ADDED,
LIBRARY_CHANGED,
LIBRARY_DELETED,
READLIST_ADDED,
READLIST_CHANGED,
READLIST_DELETED,
READPROGRESS_CHANGED,
READPROGRESS_DELETED,
SERIES_ADDED,
SERIES_CHANGED,
SERIES_DELETED,
THUMBNAILBOOK_ADDED,
THUMBNAILSERIES_ADDED,
} from "@/types/events";
import Vue from "vue";
import {TaskQueueSseDto} from "@/types/komga-sse";
const API_SSE = '/sse/v1/events'
export default class KomgaSseService {
private eventSource: EventSource | undefined
private eventHub: Vue
private store: any
constructor(eventHub: Vue, store: any) {
this.eventHub = eventHub
this.store = store
this.eventHub.$watch(
() => this.store.getters.authenticated,
(val) => {
if (val) this.connect()
else this.disconnect()
})
}
connect() {
this.eventSource = new EventSource(urls.originNoSlash + API_SSE, {withCredentials: true})
// Libraries
this.eventSource.addEventListener('LibraryAdded', (event: any) => this.emit(LIBRARY_ADDED, event))
this.eventSource.addEventListener('LibraryChanged', (event: any) => this.emit(LIBRARY_CHANGED, event))
this.eventSource.addEventListener('LibraryDeleted', (event: any) => this.emit(LIBRARY_DELETED, event))
// Series
this.eventSource.addEventListener('SeriesAdded', (event: any) => this.emit(SERIES_ADDED, event))
this.eventSource.addEventListener('SeriesChanged', (event: any) => this.emit(SERIES_CHANGED, event))
this.eventSource.addEventListener('SeriesDeleted', (event: any) => this.emit(SERIES_DELETED, event))
// Books
this.eventSource.addEventListener('BookAdded', (event: any) => this.emit(BOOK_ADDED, event))
this.eventSource.addEventListener('BookChanged', (event: any) => this.emit(BOOK_CHANGED, event))
this.eventSource.addEventListener('BookDeleted', (event: any) => this.emit(BOOK_DELETED, event))
this.eventSource.addEventListener('BookImported', (event: any) => this.emit(BOOK_IMPORTED, event))
// Collections
this.eventSource.addEventListener('CollectionAdded', (event: any) => this.emit(COLLECTION_ADDED, event))
this.eventSource.addEventListener('CollectionChanged', (event: any) => this.emit(COLLECTION_CHANGED, event))
this.eventSource.addEventListener('CollectionDeleted', (event: any) => this.emit(COLLECTION_DELETED, event))
// Read Lists
this.eventSource.addEventListener('ReadListAdded', (event: any) => this.emit(READLIST_ADDED, event))
this.eventSource.addEventListener('ReadListChanged', (event: any) => this.emit(READLIST_CHANGED, event))
this.eventSource.addEventListener('ReadListDeleted', (event: any) => this.emit(READLIST_DELETED, event))
// Read Progress
this.eventSource.addEventListener('ReadProgressChanged', (event: any) => this.emit(READPROGRESS_CHANGED, event))
this.eventSource.addEventListener('ReadProgressDeleted', (event: any) => this.emit(READPROGRESS_DELETED, event))
// Thumbnails
this.eventSource.addEventListener('ThumbnailBookAdded', (event: any) => this.emit(THUMBNAILBOOK_ADDED, event))
this.eventSource.addEventListener('ThumbnailSeriesAdded', (event: any) => this.emit(THUMBNAILSERIES_ADDED, event))
this.eventSource.addEventListener('TaskQueueStatus', (event: any) => this.updateTaskCount(event))
}
disconnect() {
this.eventSource?.close()
}
private emit(name: string, event: any) {
this.eventHub.$emit(name, JSON.parse(event.data))
}
private updateTaskCount(event: any) {
const data = JSON.parse(event.data) as TaskQueueSseDto
this.store.commit('setTaskCount', data.count)
}
}

View file

@ -23,7 +23,7 @@ export enum ReadStatus {
}
export function replaceCompositeReadStatus(list: string[]): string[] {
if(list.includes(ReadStatus.UNREAD_AND_IN_PROGRESS)){
if(list?.includes(ReadStatus.UNREAD_AND_IN_PROGRESS)){
return [...without(list, ReadStatus.UNREAD_AND_IN_PROGRESS), ReadStatus.UNREAD, ReadStatus.IN_PROGRESS]
}
else return list

View file

@ -1,37 +0,0 @@
interface EventBookChanged {
id: string,
seriesId: string
}
interface EventSeriesChanged {
id: string,
libraryId: string
}
interface EventCollectionChanged {
id: string
}
interface EventCollectionDeleted {
id: string
}
interface EventReadListChanged {
id: string
}
interface EventReadListDeleted {
id: string
}
interface EventLibraryAdded {
id: string
}
interface EventLibraryChanged {
id: string
}
interface EventLibraryDeleted {
id: string
}

View file

@ -1,68 +1,26 @@
import { BookDto } from '@/types/komga-books'
import {SeriesDto} from "@/types/komga-series";
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'
export function bookToEventBookChanged (book: BookDto): EventBookChanged {
return {
id: book.id,
seriesId: book.seriesId,
} as EventBookChanged
}
export const BOOK_ADDED = 'book-added'
export const BOOK_CHANGED = 'book-changed'
export const BOOK_DELETED = 'book-deleted'
export const BOOK_IMPORTED = 'book-imported'
export function seriesToEventSeriesChanged (series: SeriesDto): EventSeriesChanged {
return {
id: series.id,
libraryId: series.libraryId,
} as EventSeriesChanged
}
export const SERIES_ADDED = 'series-added'
export const SERIES_CHANGED = 'series-changed'
export const SERIES_DELETED = 'series-deleted'
export function collectionToEventCollectionChanged (collection: CollectionDto): EventCollectionChanged {
return {
id: collection.id,
} as EventCollectionChanged
}
export const COLLECTION_ADDED = 'collection-added'
export const COLLECTION_CHANGED = 'collection-changed'
export const COLLECTION_DELETED = 'collection-deleted'
export function collectionToEventCollectionDeleted (collection: CollectionDto): EventCollectionDeleted {
return {
id: collection.id,
} as EventCollectionDeleted
}
export const READLIST_ADDED = 'readlist-added'
export const READLIST_CHANGED = 'readlist-changed'
export const READLIST_DELETED = 'readlist-deleted'
export function readListToEventReadListChanged (readList: ReadListDto): EventReadListChanged {
return {
id: readList.id,
} as EventReadListChanged
}
export const READPROGRESS_CHANGED = 'readprogress-changed'
export const READPROGRESS_DELETED = 'readprogress-deleted'
export function readListToEventReadListDeleted (readList: ReadListDto): EventReadListDeleted {
return {
id: readList.id,
} as EventReadListDeleted
}
export function libraryToEventLibraryAdded (library: LibraryDto): EventLibraryAdded {
return {
id: library.id,
} as EventLibraryAdded
}
export function libraryToEventLibraryChanged (library: LibraryDto): EventLibraryChanged {
return {
id: library.id,
} as EventLibraryChanged
}
export function libraryToEventLibraryDeleted (library: LibraryDto): EventLibraryDeleted {
return {
id: library.id,
} as EventLibraryDeleted
}
export const THUMBNAILBOOK_ADDED = 'thumbnailbook-added'
export const THUMBNAILSERIES_ADDED = 'thumbnailbook-added'

View file

@ -0,0 +1,49 @@
export interface LibrarySseDto {
libraryId: string,
}
export interface SeriesSseDto {
seriesId: string,
libraryId: string,
}
export interface BookSseDto {
bookId: string,
seriesId: string,
libraryId: string,
}
export interface CollectionSseDto {
collectionId: string,
seriesIds: string[],
}
export interface ReadListSseDto {
readListId: string,
bookIds: string[],
}
export interface ReadProgressSseDto {
bookId: string,
userId: string,
}
export interface ThumbnailBookSseDto {
bookId: string,
seriesId: string,
}
export interface ThumbnailSeriesSseDto {
seriesId: string,
}
export interface TaskQueueSseDto {
count: number,
}
export interface BookImportSseDto {
bookId?: string,
sourceFile: string,
success: boolean,
message?: string,
}

View file

@ -322,7 +322,16 @@ import {getReadProgress, getReadProgressPercentage} from '@/functions/book-progr
import {getBookTitleCompact} from '@/functions/book-title'
import {bookFileUrl, bookThumbnailUrl} from '@/functions/urls'
import {ReadStatus} from '@/types/enum-books'
import {BOOK_CHANGED, LIBRARY_DELETED} from '@/types/events'
import {
BOOK_CHANGED,
BOOK_DELETED,
LIBRARY_DELETED,
READLIST_ADDED,
READLIST_CHANGED,
READLIST_DELETED,
READPROGRESS_CHANGED,
READPROGRESS_DELETED,
} from '@/types/events'
import Vue from 'vue'
import ReadListsExpansionPanels from '@/components/ReadListsExpansionPanels.vue'
import {BookDto, BookFormat} from '@/types/komga-books'
@ -333,6 +342,7 @@ import VueHorizontal from "vue-horizontal";
import {authorRoles} from "@/types/author-roles";
import {convertErrorCodes} from "@/functions/error-codes";
import RtlIcon from "@/components/RtlIcon.vue";
import {BookSseDto, LibrarySseDto, ReadListSseDto, ReadProgressSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'BrowseBook',
@ -351,12 +361,24 @@ export default Vue.extend({
},
async created() {
this.loadBook(this.bookId)
this.$eventHub.$on(BOOK_CHANGED, this.reloadBook)
this.$eventHub.$on(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$on(BOOK_DELETED, this.bookDeleted)
this.$eventHub.$on(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$on(READPROGRESS_DELETED, this.readProgressChanged)
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$on(READLIST_ADDED, this.readListChanged)
this.$eventHub.$on(READLIST_CHANGED, this.readListChanged)
this.$eventHub.$on(READLIST_DELETED, this.readListChanged)
},
beforeDestroy() {
this.$eventHub.$off(BOOK_CHANGED, this.reloadBook)
this.$eventHub.$off(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$off(BOOK_DELETED, this.bookDeleted)
this.$eventHub.$off(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$off(READLIST_ADDED, this.readListChanged)
this.$eventHub.$off(READLIST_CHANGED, this.readListChanged)
this.$eventHub.$off(READLIST_DELETED, this.readListChanged)
},
props: {
bookId: {
@ -434,13 +456,27 @@ export default Vue.extend({
},
},
methods: {
libraryDeleted(event: EventLibraryDeleted) {
if (event.id === this.book.libraryId) {
libraryDeleted(event: LibrarySseDto) {
if (event.libraryId === this.book.libraryId) {
this.$router.push({name: 'home'})
}
},
reloadBook(event: EventBookChanged) {
if (event.id === this.bookId) this.loadBook(this.bookId)
readListChanged(event: ReadListSseDto) {
if(event.bookIds.includes(this.bookId) || this.readLists.map(x => x.id).includes(event.readListId)){
this.$komgaBooks.getReadLists(this.bookId)
.then(v => this.readLists = v)
}
},
bookChanged(event: BookSseDto) {
if (event.bookId === this.bookId) this.loadBook(this.bookId)
},
bookDeleted(event: BookSseDto) {
if (event.bookId === this.bookId){
this.$router.push({name:'browse-series', params: {seriesId: this.series.id }})
}
},
readProgressChanged(event: ReadProgressSseDto){
if (event.bookId === this.bookId) this.loadBook(this.bookId)
},
async loadBook(bookId: string) {
this.book = await this.$komgaBooks.getBook(bookId)

View file

@ -114,7 +114,7 @@
import CollectionActionsMenu from '@/components/menus/CollectionActionsMenu.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import {COLLECTION_CHANGED, COLLECTION_DELETED, SERIES_CHANGED} from '@/types/events'
import {COLLECTION_CHANGED, COLLECTION_DELETED, SERIES_CHANGED, SERIES_DELETED} from '@/types/events'
import Vue from 'vue'
import SeriesMultiSelectBar from '@/components/bars/SeriesMultiSelectBar.vue'
import {LIBRARIES_ALL} from '@/types/library'
@ -130,6 +130,7 @@ import {parseQueryParam} from '@/functions/query-params'
import {SeriesDto} from "@/types/komga-series";
import {authorRoles} from "@/types/author-roles";
import {AuthorDto} from "@/types/komga-books";
import {CollectionSseDto, SeriesSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'BrowseCollection',
@ -186,13 +187,15 @@ export default Vue.extend({
},
created() {
this.$eventHub.$on(COLLECTION_CHANGED, this.collectionChanged)
this.$eventHub.$on(COLLECTION_DELETED, this.afterDelete)
this.$eventHub.$on(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$on(COLLECTION_DELETED, this.collectionDeleted)
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$on(SERIES_DELETED, this.seriesChanged)
},
beforeDestroy() {
this.$eventHub.$off(COLLECTION_CHANGED, this.collectionChanged)
this.$eventHub.$off(COLLECTION_DELETED, this.afterDelete)
this.$eventHub.$off(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$off(COLLECTION_DELETED, this.collectionDeleted)
this.$eventHub.$off(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$off(SERIES_DELETED, this.seriesChanged)
},
async mounted() {
await this.resetParams(this.$route, this.collectionId)
@ -345,11 +348,16 @@ export default Vue.extend({
unsetWatches() {
this.filterUnwatch()
},
collectionChanged(event: EventCollectionChanged) {
if (event.id === this.collectionId) {
collectionChanged(event: CollectionSseDto) {
if (event.collectionId === this.collectionId) {
this.loadCollection(this.collectionId)
}
},
collectionDeleted(event: CollectionSseDto) {
if(event.collectionId === this.collectionId) {
this.$router.push({name: 'browse-collections', params: {libraryId: LIBRARIES_ALL}})
}
},
updateRouteAndReload() {
this.unsetWatches()
@ -431,11 +439,8 @@ export default Vue.extend({
editCollection() {
this.$store.dispatch('dialogEditCollection', this.collection)
},
afterDelete() {
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)
seriesChanged(event: SeriesSseDto) {
if (this.series.some(s => s.id === event.seriesId)) this.loadCollection(this.collectionId)
},
},
})

View file

@ -54,10 +54,11 @@ 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, COLLECTION_DELETED, LIBRARY_CHANGED} from '@/types/events'
import {COLLECTION_ADDED, COLLECTION_CHANGED, COLLECTION_DELETED, LIBRARY_CHANGED} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
import {LibrarySseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'BrowseCollections',
@ -87,11 +88,13 @@ export default Vue.extend({
},
},
created() {
this.$eventHub.$on(COLLECTION_ADDED, this.reloadCollections)
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_ADDED, this.reloadCollections)
this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCollections)
this.$eventHub.$off(COLLECTION_DELETED, this.reloadCollections)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
@ -179,8 +182,8 @@ export default Vue.extend({
reloadCollections() {
this.loadLibrary(this.libraryId)
},
reloadLibrary(event: EventLibraryChanged) {
if (event.id === this.libraryId) {
reloadLibrary(event: LibrarySseDto) {
if (event.libraryId === this.libraryId) {
this.loadLibrary(this.libraryId)
}
},

View file

@ -112,7 +112,7 @@ import PageSizeSelect from '@/components/PageSizeSelect.vue'
import {parseQueryParam, parseQuerySort} from '@/functions/query-params'
import {ReadStatus, replaceCompositeReadStatus} from '@/types/enum-books'
import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series'
import {LIBRARY_CHANGED, LIBRARY_DELETED, SERIES_CHANGED} from '@/types/events'
import {LIBRARY_CHANGED, LIBRARY_DELETED, SERIES_ADDED, SERIES_CHANGED, SERIES_DELETED} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
@ -124,6 +124,7 @@ import {mergeFilterParams, sortOrFilterActive, toNameValue} from '@/functions/fi
import {SeriesDto} from "@/types/komga-series";
import {AuthorDto} from "@/types/komga-books";
import {authorRoles} from "@/types/author-roles";
import {LibrarySseDto, SeriesSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'BrowseLibraries',
@ -184,14 +185,18 @@ export default Vue.extend({
},
},
created() {
this.$eventHub.$on(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$on(SERIES_ADDED, this.seriesChanged)
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$on(SERIES_DELETED, this.seriesChanged)
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibrary)
this.$eventHub.$on(LIBRARY_CHANGED, this.libraryChanged)
},
beforeDestroy() {
this.$eventHub.$off(SERIES_CHANGED, this.reloadSeries)
this.$eventHub.$off(SERIES_ADDED, this.seriesChanged)
this.$eventHub.$off(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$off(SERIES_DELETED, this.seriesChanged)
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
this.$eventHub.$off(LIBRARY_CHANGED, this.libraryChanged)
},
async mounted() {
this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.BROWSE})
@ -365,8 +370,8 @@ export default Vue.extend({
})
return validFilter
},
libraryDeleted(event: EventLibraryDeleted) {
if (event.id === this.libraryId) {
libraryDeleted(event: LibrarySseDto) {
if (event.libraryId === this.libraryId) {
this.$router.push({name: 'home'})
} else if (this.libraryId === LIBRARIES_ALL) {
this.loadLibrary(this.libraryId)
@ -407,13 +412,13 @@ export default Vue.extend({
this.setWatches()
},
reloadSeries(event: EventSeriesChanged) {
seriesChanged(event: SeriesSseDto) {
if (this.libraryId === LIBRARIES_ALL || event.libraryId === this.libraryId) {
this.loadPage(this.libraryId, this.page, this.sortActive)
}
},
reloadLibrary(event: EventLibraryChanged) {
if (this.libraryId === LIBRARIES_ALL || event.id === this.libraryId) {
libraryChanged(event: LibrarySseDto) {
if (this.libraryId === LIBRARIES_ALL || event.libraryId === this.libraryId) {
this.loadLibrary(this.libraryId)
}
},

View file

@ -76,12 +76,20 @@
<script lang="ts">
import ItemBrowser from '@/components/ItemBrowser.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
import {BOOK_CHANGED, READLIST_CHANGED, READLIST_DELETED} from '@/types/events'
import {
BOOK_CHANGED,
BOOK_DELETED,
READLIST_CHANGED,
READLIST_DELETED,
READPROGRESS_CHANGED,
READPROGRESS_DELETED,
} from '@/types/events'
import Vue from 'vue'
import ReadListActionsMenu from '@/components/menus/ReadListActionsMenu.vue'
import BooksMultiSelectBar from '@/components/bars/BooksMultiSelectBar.vue'
import {BookDto, ReadProgressUpdateDto} from '@/types/komga-books'
import {ContextOrigin} from '@/types/context'
import {BookSseDto, ReadListSseDto, ReadProgressSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'BrowseReadList',
@ -122,13 +130,19 @@ export default Vue.extend({
},
created () {
this.$eventHub.$on(READLIST_CHANGED, this.readListChanged)
this.$eventHub.$on(READLIST_DELETED, this.afterDelete)
this.$eventHub.$on(BOOK_CHANGED, this.reloadBook)
this.$eventHub.$on(READLIST_DELETED, this.readListDeleted)
this.$eventHub.$on(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$on(BOOK_DELETED, this.bookChanged)
this.$eventHub.$on(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$on(READPROGRESS_DELETED, this.readProgressChanged)
},
beforeDestroy () {
this.$eventHub.$off(READLIST_CHANGED, this.readListChanged)
this.$eventHub.$off(READLIST_DELETED, this.afterDelete)
this.$eventHub.$off(BOOK_CHANGED, this.reloadBook)
this.$eventHub.$off(READLIST_DELETED, this.readListDeleted)
this.$eventHub.$off(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$off(BOOK_DELETED, this.bookChanged)
this.$eventHub.$off(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
},
mounted () {
this.loadReadList(this.readListId)
@ -150,11 +164,16 @@ export default Vue.extend({
},
},
methods: {
readListChanged (event: EventReadListChanged) {
if (event.id === this.readListId) {
readListChanged (event: ReadListSseDto) {
if (event.readListId === this.readListId) {
this.loadReadList(this.readListId)
}
},
readListDeleted (event: ReadListSseDto) {
if (event.readListId === this.readListId) {
this.$router.push({name: 'browse-readlists', params: {libraryId: 'all'}})
}
},
async loadReadList (readListId: string) {
this.$komgaReadLists.getOneReadList(readListId)
.then(v => this.readList = v)
@ -206,11 +225,11 @@ export default Vue.extend({
editReadList () {
this.$store.dispatch('dialogEditReadList', this.readList)
},
afterDelete () {
this.$router.push({ name: 'browse-readlists', params: { libraryId: 'all' } })
bookChanged (event: BookSseDto) {
if (this.books.some(b => b.id === event.bookId)) this.loadReadList(this.readListId)
},
reloadBook (event: EventBookChanged) {
if (this.books.some(b => b.id === event.id)) this.loadReadList(this.readListId)
readProgressChanged(event: ReadProgressSseDto){
if (this.books.some(b => b.id === event.bookId)) this.loadReadList(this.readListId)
},
},
})

View file

@ -54,10 +54,11 @@ 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 {LIBRARY_CHANGED, READLIST_CHANGED, READLIST_DELETED} from '@/types/events'
import {LIBRARY_CHANGED, READLIST_ADDED, READLIST_CHANGED, READLIST_DELETED} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
import {LibrarySseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'BrowseReadLists',
@ -87,11 +88,13 @@ export default Vue.extend({
},
},
created () {
this.$eventHub.$on(READLIST_ADDED, this.reloadElements)
this.$eventHub.$on(READLIST_CHANGED, this.reloadElements)
this.$eventHub.$on(READLIST_DELETED, this.reloadElements)
this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibrary)
},
beforeDestroy () {
this.$eventHub.$off(READLIST_ADDED, this.reloadElements)
this.$eventHub.$off(READLIST_CHANGED, this.reloadElements)
this.$eventHub.$off(READLIST_DELETED, this.reloadElements)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
@ -179,8 +182,8 @@ export default Vue.extend({
reloadElements () {
this.loadLibrary(this.libraryId)
},
reloadLibrary (event: EventLibraryChanged) {
if (event.id === this.libraryId) {
reloadLibrary (event: LibrarySseDto) {
if (event.libraryId === this.libraryId) {
this.loadLibrary(this.libraryId)
}
},

View file

@ -381,7 +381,19 @@ import PageSizeSelect from '@/components/PageSizeSelect.vue'
import {parseQueryParam, parseQueryParamAndFilter, parseQuerySort} from '@/functions/query-params'
import {seriesFileUrl, seriesThumbnailUrl} from '@/functions/urls'
import {ReadStatus, replaceCompositeReadStatus} from '@/types/enum-books'
import {BOOK_CHANGED, LIBRARY_DELETED, READLIST_CHANGED, SERIES_CHANGED} from '@/types/events'
import {
BOOK_ADDED,
BOOK_CHANGED,
BOOK_DELETED,
COLLECTION_ADDED,
COLLECTION_CHANGED,
COLLECTION_DELETED,
LIBRARY_DELETED,
READPROGRESS_CHANGED,
READPROGRESS_DELETED,
SERIES_CHANGED,
SERIES_DELETED,
} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {AuthorDto, BookDto} from '@/types/komga-books'
@ -397,6 +409,8 @@ import ReadMore from "@/components/ReadMore.vue";
import {authorRoles, authorRolesSeries} from "@/types/author-roles";
import VueHorizontal from "vue-horizontal";
import RtlIcon from "@/components/RtlIcon.vue";
import {throttle} from "lodash";
import {BookSseDto, CollectionSseDto, LibrarySseDto, ReadProgressSseDto, SeriesSseDto} from "@/types/komga-sse";
const tags = require('language-tags')
@ -542,16 +556,30 @@ 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(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$on(SERIES_DELETED, this.seriesDeleted)
this.$eventHub.$on(BOOK_ADDED, this.bookChanged)
this.$eventHub.$on(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$on(BOOK_DELETED, this.bookChanged)
this.$eventHub.$on(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$on(READPROGRESS_DELETED, this.readProgressChanged)
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$on(COLLECTION_ADDED, this.collectionChanged)
this.$eventHub.$on(COLLECTION_CHANGED, this.collectionChanged)
this.$eventHub.$on(COLLECTION_DELETED, this.collectionChanged)
},
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(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$off(SERIES_DELETED, this.seriesDeleted)
this.$eventHub.$off(BOOK_ADDED, this.bookChanged)
this.$eventHub.$off(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$off(BOOK_DELETED, this.bookChanged)
this.$eventHub.$off(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$off(COLLECTION_ADDED, this.collectionChanged)
this.$eventHub.$off(COLLECTION_CHANGED, this.collectionChanged)
this.$eventHub.$off(COLLECTION_DELETED, this.collectionChanged)
},
async mounted() {
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
@ -636,22 +664,41 @@ export default Vue.extend({
this.setWatches()
},
libraryDeleted(event: EventLibraryDeleted) {
if (event.id === this.series.libraryId) {
libraryDeleted(event: LibrarySseDto) {
if (event.libraryId === this.series.libraryId) {
this.$router.push({name: 'home'})
}
},
reloadSeries(event: EventSeriesChanged) {
if (event.id === this.seriesId) this.loadSeries(this.seriesId)
seriesChanged(event: SeriesSseDto) {
if (event.seriesId === this.seriesId)
this.$komgaSeries.getOneSeries(this.seriesId)
.then(v => this.series = v)
},
reloadBooks(event: EventBookChanged) {
if (event.seriesId === this.seriesId) this.loadSeries(this.seriesId)
seriesDeleted(event: SeriesSseDto) {
if (event.seriesId === this.seriesId) {
this.$router.push({name: 'browse-libraries', params: {libraryId: this.series.libraryId}})
}
},
bookChanged(event: BookSseDto) {
if (event.seriesId === this.seriesId) this.reloadPage()
},
readProgressChanged(event: ReadProgressSseDto) {
if (this.books.some(b => b.id === event.bookId)) this.reloadPage()
},
collectionChanged(event: CollectionSseDto) {
if (event.seriesIds.includes(this.seriesId) || this.collections.map(x => x.id).includes(event.collectionId)) {
this.$komgaSeries.getCollections(this.seriesId)
.then(v => this.collections = v)
}
},
reloadPage: throttle(function (this: any) {
this.loadPage(this.seriesId, this.page, this.sortActive)
}, 5000),
async loadSeries(seriesId: string) {
this.$komgaSeries.getOneSeries(seriesId)
.then(v => this.series = v)
.then(v => this.series = v)
this.$komgaSeries.getCollections(seriesId)
.then(v => this.collections = v)
.then(v => this.collections = v)
await this.loadPage(seriesId, this.page, this.sortActive)
},

View file

@ -17,7 +17,8 @@
</toolbar-sticky>
<library-navigation v-if="individualLibrary && $vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<library-navigation v-if="individualLibrary && $vuetify.breakpoint.name === 'xs'" :libraryId="libraryId"
bottom-navigation/>
<series-multi-select-bar
v-model="selectedSeries"
@ -134,10 +135,21 @@ import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
import LibraryNavigation from '@/components/LibraryNavigation.vue'
import {ReadStatus} from '@/types/enum-books'
import {BookDto} from '@/types/komga-books'
import {BOOK_CHANGED, LIBRARY_DELETED, SERIES_CHANGED} from '@/types/events'
import {
BOOK_ADDED,
BOOK_CHANGED,
BOOK_DELETED,
READPROGRESS_CHANGED,
READPROGRESS_DELETED,
SERIES_ADDED,
SERIES_CHANGED,
SERIES_DELETED,
} from '@/types/events'
import Vue from 'vue'
import {SeriesDto} from "@/types/komga-series";
import {LIBRARIES_ALL, LIBRARY_ROUTE} from "@/types/library";
import {throttle} from 'lodash'
import {BookSseDto, ReadProgressSseDto, SeriesSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'Dashboard',
@ -164,17 +176,30 @@ export default Vue.extend({
}
},
created() {
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$on(SERIES_CHANGED, this.reload)
this.$eventHub.$on(BOOK_CHANGED, this.reload)
this.$eventHub.$on(SERIES_ADDED, this.seriesChanged)
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$on(SERIES_DELETED, this.seriesChanged)
this.$eventHub.$on(BOOK_ADDED, this.bookChanged)
this.$eventHub.$on(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$on(BOOK_DELETED, this.bookChanged)
this.$eventHub.$on(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$on(READPROGRESS_DELETED, this.readProgressChanged)
},
beforeDestroy() {
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$off(SERIES_CHANGED, this.reload)
this.$eventHub.$off(BOOK_CHANGED, this.reload)
this.$eventHub.$off(SERIES_ADDED, this.seriesChanged)
this.$eventHub.$off(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$off(SERIES_DELETED, this.seriesChanged)
this.$eventHub.$off(BOOK_ADDED, this.bookChanged)
this.$eventHub.$off(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$off(BOOK_DELETED, this.bookChanged)
this.$eventHub.$off(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
},
mounted() {
if(this.individualLibrary) this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.RECOMMENDED})
if (this.individualLibrary) this.$store.commit('setLibraryRoute', {
id: this.libraryId,
route: LIBRARY_ROUTE.RECOMMENDED,
})
this.reload()
},
props: {
@ -193,6 +218,12 @@ export default Vue.extend({
libraryId(val) {
this.loadAll(val)
},
'$store.state.komgaLibraries.libraries': {
handler(val){
if(val.length === 0) this.$router.push({name: 'welcome'})
else this.reload()
},
},
},
computed: {
fixedCardWidth(): number {
@ -213,16 +244,24 @@ export default Vue.extend({
getRequestLibraryId(libraryId: string): string | undefined {
return libraryId !== LIBRARIES_ALL ? libraryId : undefined
},
libraryDeleted() {
if (this.$store.state.komgaLibraries.libraries.length === 0) {
this.$router.push({name: 'welcome'})
} else {
seriesChanged(event: SeriesSseDto) {
if (this.libraryId === LIBRARIES_ALL || event.libraryId === this.libraryId) {
this.reload()
}
},
reload() {
this.loadAll(this.libraryId)
bookChanged(event: BookSseDto){
if (this.libraryId === LIBRARIES_ALL || event.libraryId === this.libraryId) {
this.reload()
}
},
readProgressChanged(event: ReadProgressSseDto){
if (this.inProgressBooks.some(b => b.id === event.bookId)) this.reload()
else if (this.latestBooks.some(b => b.id === event.bookId)) this.reload()
else if (this.onDeckBooks.some(b => b.id === event.bookId)) this.reload()
},
reload: throttle(function(this: any) {
this.loadAll(this.libraryId)
}, 5000),
loadAll(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
this.selectedSeries = []

View file

@ -20,6 +20,21 @@
Komga
</v-list-item-title>
</v-list-item-content>
<v-tooltip left>
<template v-slot:activator="{ on }">
<v-progress-linear
:active="taskCount > 0"
indeterminate
absolute
bottom
height="2"
color="secondary"
v-on="on"
/>
</template>
<span>{{ $tc('common.pending_tasks', taskCount) }}</span>
</v-tooltip>
</v-list-item>
<v-divider/>
@ -141,6 +156,7 @@
<v-main class="fill-height">
<dialogs/>
<toaster/>
<router-view/>
</v-main>
</div>
@ -152,11 +168,12 @@ import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
import SearchBox from '@/components/SearchBox.vue'
import {Theme} from '@/types/themes'
import Vue from 'vue'
import {LIBRARIES_ALL} from "@/types/library";
import {LIBRARIES_ALL} from "@/types/library"
import Toaster from "@/components/Toaster.vue"
export default Vue.extend({
name: 'home',
components: {LibraryActionsMenu, SearchBox, Dialogs},
components: {Toaster, LibraryActionsMenu, SearchBox, Dialogs},
data: function () {
return {
LIBRARIES_ALL,
@ -171,6 +188,9 @@ export default Vue.extend({
}
},
computed: {
taskCount(): number {
return this.$store.state.komgaSse.taskCount
},
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},

View file

@ -110,15 +110,20 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
import {BookDto} from '@/types/komga-books'
import {
BOOK_CHANGED,
BOOK_DELETED,
COLLECTION_CHANGED,
COLLECTION_DELETED,
LIBRARY_DELETED,
READLIST_CHANGED,
READLIST_DELETED,
READPROGRESS_CHANGED,
READPROGRESS_DELETED,
SERIES_CHANGED,
SERIES_DELETED,
} from '@/types/events'
import Vue from 'vue'
import {SeriesDto} from "@/types/komga-series";
import {BookSseDto, CollectionSseDto, ReadListSseDto, ReadProgressSseDto, SeriesSseDto} from "@/types/komga-sse";
export default Vue.extend({
name: 'Search',
@ -144,21 +149,29 @@ export default Vue.extend({
},
created () {
this.$eventHub.$on(LIBRARY_DELETED, this.reloadResults)
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)
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$on(SERIES_DELETED, this.seriesChanged)
this.$eventHub.$on(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$on(BOOK_DELETED, this.bookChanged)
this.$eventHub.$on(COLLECTION_CHANGED, this.collectionChanged)
this.$eventHub.$on(COLLECTION_DELETED, this.collectionChanged)
this.$eventHub.$on(READLIST_CHANGED, this.readListChanged)
this.$eventHub.$on(READLIST_DELETED, this.readListChanged)
this.$eventHub.$on(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$on(READPROGRESS_DELETED, this.readProgressChanged)
},
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)
this.$eventHub.$off(SERIES_CHANGED, this.seriesChanged)
this.$eventHub.$off(SERIES_DELETED, this.seriesChanged)
this.$eventHub.$off(BOOK_CHANGED, this.bookChanged)
this.$eventHub.$off(BOOK_DELETED, this.bookChanged)
this.$eventHub.$off(COLLECTION_CHANGED, this.collectionChanged)
this.$eventHub.$off(COLLECTION_DELETED, this.collectionChanged)
this.$eventHub.$off(READLIST_CHANGED, this.readListChanged)
this.$eventHub.$off(READLIST_DELETED, this.readListChanged)
this.$eventHub.$off(READPROGRESS_CHANGED, this.readProgressChanged)
this.$eventHub.$off(READPROGRESS_DELETED, this.readProgressChanged)
},
watch: {
'$route.query.q': {
@ -199,6 +212,31 @@ export default Vue.extend({
},
},
methods: {
seriesChanged(event: SeriesSseDto){
if(this.series.map(x => x.id).includes(event.seriesId)){
this.reloadResults()
}
},
bookChanged(event: BookSseDto){
if(this.books.map(x => x.id).includes(event.bookId)){
this.reloadResults()
}
},
readProgressChanged(event: ReadProgressSseDto){
if(this.books.map(x => x.id).includes(event.bookId)){
this.reloadResults()
}
},
collectionChanged (event: CollectionSseDto) {
if (this.collections.map(x => x.id).includes(event.collectionId)) {
this.reloadResults()
}
},
readListChanged (event: ReadListSseDto) {
if (this.readLists.map(x => x.id).includes(event.readListId)) {
this.reloadResults()
}
},
singleEditSeries (series: SeriesDto) {
this.$store.dispatch('dialogUpdateSeries', series)
},

View file

@ -1,5 +1,5 @@
<template>
<div class="ma-3">
<div class="pa-6">
<v-row align="center" justify="center">
<v-img src="../assets/logo.svg"
max-width="400"
@ -16,7 +16,6 @@
</template>
<script lang="ts">
import {LIBRARY_ADDED} from '@/types/events'
import Vue from 'vue'
export default Vue.extend({
@ -26,21 +25,19 @@ export default Vue.extend({
return this.$store.getters.meAdmin
},
},
created () {
this.$eventHub.$on(LIBRARY_ADDED, this.libraryAdded)
},
beforeDestroy () {
this.$eventHub.$off(LIBRARY_ADDED, this.libraryAdded)
},
mounted () {
if (this.$store.state.komgaLibraries.libraries.length !== 0) {
this.$router.push({ name: 'dashboard' })
}
},
methods: {
libraryAdded () {
this.$router.push({ name: 'dashboard' })
watch: {
'$store.state.komgaLibraries.libraries': {
handler(val){
if(val.length !== 0) this.$router.push({name: 'dashboard'})
},
},
},
methods: {
addLibrary () {
this.$store.dispatch('dialogAddLibrary')
},