Merge pull request #494 from edwinbadillo/recommended-library

feat: Added a 'Recommended' tab in the library views for a library specific dashboard like the home page
This commit is contained in:
Gauthier 2021-04-27 22:19:57 +08:00 committed by GitHub
commit 638f40831f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 331 additions and 140 deletions

View file

@ -1,31 +1,78 @@
<template>
<v-bottom-navigation
v-if="collectionsCount > 0 || readListsCount > 0"
grow color="primary"
:fixed="$vuetify.breakpoint.name === 'xs'"
>
<v-btn :to="{name: 'browse-libraries', params: {libraryId: libraryId}}">
<span>{{ $t('library_navigation.browse') }}</span>
<v-icon>mdi-bookshelf</v-icon>
</v-btn>
<v-btn
v-if="collectionsCount > 0"
:to="{name: 'browse-collections', params: {libraryId: libraryId}}"
<div>
<v-bottom-navigation
v-if="show && bottomNavigation"
grow color="primary"
:fixed="$vuetify.breakpoint.name === 'xs'"
>
<span>{{ $t('library_navigation.collections') }}</span>
<v-icon>mdi-layers-triple</v-icon>
</v-btn>
<v-btn v-if="showRecommended"
:to="{name: 'recommended-libraries', params: {libraryId: libraryId}}"
>
<span>{{ $t('library_navigation.recommended') }}</span>
<v-icon>mdi-star</v-icon>
</v-btn>
<v-btn
v-if="readListsCount > 0"
:to="{name: 'browse-readlists', params: {libraryId: libraryId}}"
<v-btn :to="{name: 'browse-libraries', params: {libraryId: libraryId}}">
<span>{{ $t('library_navigation.browse') }}</span>
<v-icon>mdi-bookshelf</v-icon>
</v-btn>
<v-btn
v-if="collectionsCount > 0"
:to="{name: 'browse-collections', params: {libraryId: libraryId}}"
>
<span>{{ $t('library_navigation.collections') }}</span>
<v-icon>mdi-layers-triple</v-icon>
</v-btn>
<v-btn
v-if="readListsCount > 0"
:to="{name: 'browse-readlists', params: {libraryId: libraryId}}"
>
<span>{{ $t('library_navigation.readlists') }}</span>
<v-icon>mdi-book-multiple</v-icon>
</v-btn>
</v-bottom-navigation>
<template
v-if="show && !bottomNavigation"
>
<span>{{ $t('library_navigation.readlists') }}</span>
<v-icon>mdi-book-multiple</v-icon>
</v-btn>
<v-btn v-if="showRecommended"
:to="{name: 'recommended-libraries', params: {libraryId: libraryId}}"
text
class="mx-1"
>
{{ $t('library_navigation.recommended') }}
</v-btn>
</v-bottom-navigation>
<v-btn :to="{name: 'browse-libraries', params: {libraryId: libraryId}}"
text
class="mx-1"
>
{{ $t('library_navigation.browse') }}
</v-btn>
<v-btn
v-if="collectionsCount > 0"
:to="{name: 'browse-collections', params: {libraryId: libraryId}}"
text
class="mx-1"
>
{{ $t('library_navigation.collections') }}
</v-btn>
<v-btn
v-if="readListsCount > 0"
:to="{name: 'browse-readlists', params: {libraryId: libraryId}}"
text
class="mx-1"
>
{{ $t('library_navigation.readlists') }}
</v-btn>
</template>
</div>
</template>
<script lang="ts">
@ -46,31 +93,43 @@ export default Vue.extend({
type: String,
required: true,
},
bottomNavigation: {
type: Boolean,
default: false,
},
},
watch: {
libraryId: {
handler (val) {
handler(val) {
this.loadCounts(val)
},
immediate: true,
},
},
created () {
created() {
this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCounts)
this.$eventHub.$on(READLIST_CHANGED, this.reloadCounts)
},
beforeDestroy () {
beforeDestroy() {
this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCounts)
this.$eventHub.$off(READLIST_CHANGED, this.reloadCounts)
},
computed: {
showRecommended(): boolean {
return this.libraryId !== LIBRARIES_ALL
},
show(): boolean {
return this.collectionsCount > 0 || this.readListsCount > 0 || this.showRecommended
},
},
methods: {
reloadCounts () {
reloadCounts() {
this.loadCounts(this.libraryId)
},
async loadCounts (libraryId: string) {
async loadCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
this.collectionsCount = (await this.$komgaCollections.getCollections(lib, { size: 1 })).totalElements
this.readListsCount = (await this.$komgaReadLists.getReadLists(lib, { size: 1 })).totalElements
this.collectionsCount = (await this.$komgaCollections.getCollections(lib, {size: 1})).totalElements
this.readListsCount = (await this.$komgaReadLists.getReadLists(lib, {size: 1})).totalElements
},
},
})

View file

@ -462,7 +462,8 @@
"library_navigation": {
"browse": "Browse",
"collections": "Collections",
"readlists": "Read Lists"
"readlists": "Read Lists",
"recommended": "Recommended"
},
"login": {
"create_user_account": "Create user account",

View file

@ -26,6 +26,7 @@ export const persistedModule: Module<any, any> = {
library: {
filter: {},
sort: {},
route: {},
},
},
getters: {
@ -38,6 +39,9 @@ export const persistedModule: Module<any, any> = {
getLibrarySort: (state) => (id: string) => {
return state.library.sort[id]
},
getLibraryRoute: (state) => (id: string) => {
return state.library.route[id]
},
},
mutations: {
setLocale(state, val) {
@ -82,5 +86,8 @@ export const persistedModule: Module<any, any> = {
setLibrarySort(state, {id, sort}) {
state.library.sort[id] = sort
},
setLibraryRoute(state, {id, route}) {
state.library.route[id] = route
},
},
}

View file

@ -2,22 +2,37 @@ import urls from '@/functions/urls'
import Vue from 'vue'
import Router from 'vue-router'
import store from './store'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from "@/types/library";
Vue.use(Router)
const lStore = store as any
const adminGuard = (to: any, from: any, next: any) => {
if (!lStore.getters.meAdmin) next({ name: 'home' })
if (!lStore.getters.meAdmin) next({name: 'home'})
else next()
}
const noLibraryGuard = (to: any, from: any, next: any) => {
if (lStore.state.komgaLibraries.libraries.length === 0) {
next({ name: 'welcome' })
next({name: 'welcome'})
} else next()
}
const getLibraryRoute = (libraryId: string) => {
switch ((lStore.getters.getLibraryRoute(libraryId) as LIBRARY_ROUTE)) {
case LIBRARY_ROUTE.COLLECTIONS:
return 'browse-collections'
case LIBRARY_ROUTE.READLISTS:
return 'browse-readlists'
case LIBRARY_ROUTE.BROWSE:
return 'browse-libraries'
case LIBRARY_ROUTE.RECOMMENDED:
default:
return 'recommended-libraries'
}
}
const router = new Router({
mode: 'history',
base: urls.base,
@ -25,7 +40,7 @@ const router = new Router({
{
path: '/',
name: 'home',
redirect: { name: 'dashboard' },
redirect: {name: 'dashboard'},
component: () => import(/* webpackChunkName: "home" */ './views/Home.vue'),
children: [
{
@ -42,7 +57,7 @@ const router = new Router({
{
path: '/settings',
name: 'settings',
redirect: { name: 'settings-analysis' },
redirect: {name: 'settings-analysis'},
component: () => import(/* webpackChunkName: "settings" */ './views/Settings.vue'),
children: [
{
@ -83,50 +98,65 @@ const router = new Router({
name: 'account',
component: () => import(/* webpackChunkName: "account" */ './views/AccountSettings.vue'),
},
{
path: '/libraries/:libraryId?',
name: 'libraries',
redirect: (route) => ({
name: getLibraryRoute(route.params.libraryId || LIBRARIES_ALL),
params: {libraryId: route.params.libraryId || LIBRARIES_ALL},
}),
},
{
path: '/libraries/:libraryId/recommended',
name: 'recommended-libraries',
beforeEnter: noLibraryGuard,
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue'),
props: (route) => ({libraryId: route.params.libraryId}),
},
{
path: '/libraries/:libraryId/series',
name: 'browse-libraries',
beforeEnter: noLibraryGuard,
component: () => import(/* webpackChunkName: "browse-libraries" */ './views/BrowseLibraries.vue'),
props: (route) => ({ libraryId: route.params.libraryId }),
props: (route) => ({libraryId: route.params.libraryId}),
},
{
path: '/libraries/:libraryId/collections',
name: 'browse-collections',
beforeEnter: noLibraryGuard,
component: () => import(/* webpackChunkName: "browse-collections" */ './views/BrowseCollections.vue'),
props: (route) => ({ libraryId: route.params.libraryId }),
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 }),
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 }),
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 }),
props: (route) => ({readListId: route.params.readListId}),
},
{
path: '/series/:seriesId',
name: 'browse-series',
component: () => import(/* webpackChunkName: "browse-series" */ './views/BrowseSeries.vue'),
props: (route) => ({ seriesId: route.params.seriesId }),
props: (route) => ({seriesId: route.params.seriesId}),
},
{
path: '/book/:bookId',
name: 'browse-book',
component: () => import(/* webpackChunkName: "browse-book" */ './views/BrowseBook.vue'),
props: (route) => ({ bookId: route.params.bookId }),
props: (route) => ({bookId: route.params.bookId}),
},
{
path: '/search',
@ -155,7 +185,7 @@ const router = new Router({
path: '/book/:bookId/read',
name: 'read-book',
component: () => import(/* webpackChunkName: "read-book" */ './views/BookReader.vue'),
props: (route) => ({ bookId: route.params.bookId }),
props: (route) => ({bookId: route.params.bookId}),
},
{
path: '*',
@ -163,12 +193,12 @@ const router = new Router({
component: () => import(/* webpackChunkName: "notfound" */ './views/PageNotFound.vue'),
},
],
scrollBehavior (to, from, savedPosition) {
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
if (to.name !== from.name) {
return { x: 0, y: 0 }
return {x: 0, y: 0}
}
}
},
@ -179,7 +209,7 @@ router.beforeEach((to, from, next) => {
document.title = 'Komga'
}
if (to.name !== 'startup' && to.name !== 'login' && !lStore.getters.authenticated) {
next({ name: 'startup', query: { redirect: to.fullPath } })
next({name: 'startup', query: {redirect: to.fullPath}})
} else next()
})

View file

@ -40,10 +40,14 @@ export default class KomgaBooksService {
}
}
async getBooksOnDeck (pageRequest?: PageRequest): Promise<Page<BookDto>> {
async getBooksOnDeck (libraryId?: string, pageRequest?: PageRequest): Promise<Page<BookDto>> {
try {
const params = { ...pageRequest } as any
if (libraryId) {
params.library_id = libraryId
}
return (await this.http.get(`${API_BOOKS}/ondeck`, {
params: { ...pageRequest },
params: params,
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve books on deck'

View file

@ -43,9 +43,12 @@ export default class KomgaSeriesService {
}
}
async getNewSeries (pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
async getNewSeries (libraryId?: string, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = { ...pageRequest } as any
if (libraryId) {
params.library_id = libraryId
}
return (await this.http.get(`${API_SERIES}/new`, {
params: params,
})).data
@ -58,9 +61,12 @@ export default class KomgaSeriesService {
}
}
async getUpdatedSeries (pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
async getUpdatedSeries (libraryId?: string, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = { ...pageRequest } as any
if (libraryId) {
params.library_id = libraryId
}
return (await this.http.get(`${API_SERIES}/updated`, {
params: params,
})).data

View file

@ -1 +1,8 @@
export const LIBRARIES_ALL = 'all'
export enum LIBRARY_ROUTE {
BROWSE = 'BROWSE',
RECOMMENDED = 'RECOMMENDED',
COLLECTIONS = 'COLLECTIONS',
READLISTS = 'READLISTS'
}

View file

@ -14,10 +14,14 @@
<v-spacer/>
<library-navigation v-if="$vuetify.breakpoint.name !== 'xs'" :libraryId="libraryId"/>
<v-spacer/>
<page-size-select v-model="pageSize"/>
</toolbar-sticky>
<library-navigation :libraryId="libraryId"/>
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<v-container fluid>
<v-pagination
@ -53,7 +57,7 @@ import PageSizeSelect from '@/components/PageSizeSelect.vue'
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'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
export default Vue.extend({
name: 'BrowseCollections',
@ -82,17 +86,18 @@ export default Vue.extend({
default: LIBRARIES_ALL,
},
},
created () {
created() {
this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCollections)
this.$eventHub.$on(COLLECTION_DELETED, this.reloadCollections)
this.$eventHub.$on(LIBRARY_CHANGED, this.reloadLibrary)
},
beforeDestroy () {
beforeDestroy() {
this.$eventHub.$off(COLLECTION_CHANGED, this.reloadCollections)
this.$eventHub.$off(COLLECTION_DELETED, this.reloadCollections)
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
},
mounted () {
mounted() {
this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.COLLECTIONS})
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
// restore from query param
@ -103,7 +108,7 @@ export default Vue.extend({
this.setWatches()
},
beforeRouteUpdate (to, from, next) {
beforeRouteUpdate(to, from, next) {
if (to.params.libraryId !== from.params.libraryId) {
// reset
this.page = 1
@ -117,10 +122,10 @@ export default Vue.extend({
next()
},
computed: {
isAdmin (): boolean {
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
paginationVisible (): number {
paginationVisible(): number {
switch (this.$vuetify.breakpoint.name) {
case 'xs':
return 5
@ -135,7 +140,7 @@ export default Vue.extend({
},
},
methods: {
setWatches () {
setWatches() {
this.pageSizeUnwatch = this.$watch('pageSize', (val) => {
this.$store.commit('setBrowsingPageSize', val)
this.updateRouteAndReload()
@ -146,11 +151,11 @@ export default Vue.extend({
this.loadPage(this.libraryId, val)
})
},
unsetWatches () {
unsetWatches() {
this.pageUnwatch()
this.pageSizeUnwatch()
},
updateRouteAndReload () {
updateRouteAndReload() {
this.unsetWatches()
this.page = 1
@ -160,10 +165,10 @@ export default Vue.extend({
this.setWatches()
},
updateRoute () {
updateRoute() {
this.$router.replace({
name: this.$route.name,
params: { libraryId: this.$route.params.libraryId },
params: {libraryId: this.$route.params.libraryId},
query: {
page: `${this.page}`,
pageSize: `${this.pageSize}`,
@ -171,23 +176,23 @@ export default Vue.extend({
} as Location).catch((_: any) => {
})
},
reloadCollections () {
reloadCollections() {
this.loadLibrary(this.libraryId)
},
reloadLibrary (event: EventLibraryChanged) {
reloadLibrary(event: EventLibraryChanged) {
if (event.id === this.libraryId) {
this.loadLibrary(this.libraryId)
}
},
async loadLibrary (libraryId: string) {
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
await this.loadPage(libraryId, this.page)
if (this.totalElements === 0) {
await this.$router.push({ name: 'browse-libraries', params: { libraryId: libraryId.toString() } })
await this.$router.push({name: 'browse-libraries', params: {libraryId: libraryId.toString()}})
}
},
async loadPage (libraryId: string, page: number) {
async loadPage(libraryId: string, page: number) {
const pageRequest = {
page: page - 1,
size: this.pageSize,
@ -200,14 +205,14 @@ export default Vue.extend({
this.totalElements = collectionsPage.totalElements
this.collections = collectionsPage.content
},
getLibraryLazy (libraryId: string): LibraryDto | undefined {
getLibraryLazy(libraryId: string): LibraryDto | undefined {
if (libraryId !== LIBRARIES_ALL) {
return this.$store.getters.getLibraryById(libraryId)
} else {
return undefined
}
},
editSingleCollection (collection: CollectionDto) {
editSingleCollection(collection: CollectionDto) {
this.$store.dispatch('dialogEditCollection', collection)
},
},

View file

@ -14,6 +14,10 @@
<v-spacer/>
<library-navigation v-if="$vuetify.breakpoint.name !== 'xs'" :libraryId="libraryId"/>
<v-spacer/>
<page-size-select v-model="pageSize"/>
<v-btn icon @click="drawer = !drawer">
@ -30,7 +34,7 @@
@edit="editMultipleSeries"
/>
<library-navigation :libraryId="libraryId"/>
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<filter-drawer v-model="drawer">
<template v-slot:default>
@ -106,7 +110,7 @@ import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series'
import {LIBRARY_CHANGED, LIBRARY_DELETED, SERIES_CHANGED} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL} from '@/types/library'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
import FilterDrawer from '@/components/FilterDrawer.vue'
import SortList from '@/components/SortList.vue'
import FilterPanels from '@/components/FilterPanels.vue'
@ -186,6 +190,7 @@ export default Vue.extend({
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
},
async mounted() {
this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.BROWSE})
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
// restore from query param
@ -428,8 +433,8 @@ export default Vue.extend({
this.totalElements = seriesPage.totalElements
this.series = seriesPage.content
},
getLibraryLazy(libraryId: any): LibraryDto | undefined {
if (libraryId !== 0) {
getLibraryLazy (libraryId: string): LibraryDto | undefined {
if (libraryId !== LIBRARIES_ALL) {
return this.$store.getters.getLibraryById(libraryId)
} else {
return undefined

View file

@ -14,10 +14,14 @@
<v-spacer/>
<library-navigation v-if="$vuetify.breakpoint.name !== 'xs'" :libraryId="libraryId"/>
<v-spacer/>
<page-size-select v-model="pageSize"/>
</toolbar-sticky>
<library-navigation :libraryId="libraryId"/>
<library-navigation v-if="$vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<v-container fluid>
<v-pagination
@ -53,7 +57,7 @@ import PageSizeSelect from '@/components/PageSizeSelect.vue'
import {LIBRARY_CHANGED, READLIST_CHANGED, READLIST_DELETED} from '@/types/events'
import Vue from 'vue'
import {Location} from 'vue-router'
import {LIBRARIES_ALL} from '@/types/library'
import {LIBRARIES_ALL, LIBRARY_ROUTE} from '@/types/library'
export default Vue.extend({
name: 'BrowseReadLists',
@ -93,6 +97,7 @@ export default Vue.extend({
this.$eventHub.$off(LIBRARY_CHANGED, this.reloadLibrary)
},
mounted () {
this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.READLISTS})
this.pageSize = this.$store.state.persistedState.browsingPageSize || this.pageSize
// restore from query param

View file

@ -1,5 +1,24 @@
<template>
<div>
<div :style="$vuetify.breakpoint.name === 'xs' ? 'margin-bottom: 56px' : undefined">
<toolbar-sticky v-if="showLibraryBar && selectedSeries.length === 0">
<!-- Action menu -->
<library-actions-menu v-if="library"
:library="library"/>
<v-toolbar-title>
<span>{{ library ? library.name : $t('common.all_libraries') }}</span>
</v-toolbar-title>
<v-spacer/>
<library-navigation v-if="showLibraryBar && $vuetify.breakpoint.name !== 'xs'" :libraryId="libraryId"/>
<v-spacer/>
</toolbar-sticky>
<library-navigation v-if="showLibraryBar && $vuetify.breakpoint.name === 'xs'" :libraryId="libraryId" bottom-navigation/>
<series-multi-select-bar
v-model="selectedSeries"
@unselect-all="selectedSeries = []"
@ -110,23 +129,31 @@ import SeriesMultiSelectBar from '@/components/bars/SeriesMultiSelectBar.vue'
import EmptyState from '@/components/EmptyState.vue'
import HorizontalScroller from '@/components/HorizontalScroller.vue'
import ItemBrowser from '@/components/ItemBrowser.vue'
import ToolbarSticky from '@/components/bars/ToolbarSticky.vue'
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 Vue from 'vue'
import {SeriesDto} from "@/types/komga-series";
import {LIBRARIES_ALL, LIBRARY_ROUTE} from "@/types/library";
export default Vue.extend({
name: 'Dashboard',
components: {
HorizontalScroller,
EmptyState,
ToolbarSticky,
LibraryNavigation,
ItemBrowser,
BooksMultiSelectBar,
SeriesMultiSelectBar,
LibraryActionsMenu,
},
data: () => {
return {
library: undefined as LibraryDto | undefined,
newSeries: [] as SeriesDto[],
updatedSeries: [] as SeriesDto[],
latestBooks: [] as BookDto[],
@ -136,57 +163,81 @@ export default Vue.extend({
selectedBooks: [] as BookDto[],
}
},
created () {
created() {
this.$eventHub.$on(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$on(SERIES_CHANGED, this.loadAll)
this.$eventHub.$on(BOOK_CHANGED, this.loadAll)
this.$eventHub.$on(SERIES_CHANGED, this.reload)
this.$eventHub.$on(BOOK_CHANGED, this.reload)
},
beforeDestroy () {
beforeDestroy() {
this.$eventHub.$off(LIBRARY_DELETED, this.libraryDeleted)
this.$eventHub.$off(SERIES_CHANGED, this.loadAll)
this.$eventHub.$off(BOOK_CHANGED, this.loadAll)
this.$eventHub.$off(SERIES_CHANGED, this.reload)
this.$eventHub.$off(BOOK_CHANGED, this.reload)
},
mounted () {
this.loadAll()
mounted() {
if(this.showLibraryBar) this.$store.commit('setLibraryRoute', {id: this.libraryId, route: LIBRARY_ROUTE.RECOMMENDED})
this.reload()
},
props: {
libraryId: {
type: String,
default: LIBRARIES_ALL,
},
},
watch: {
selectedSeries (val: SeriesDto[]) {
selectedSeries(val: SeriesDto[]) {
val.forEach(i => this.replaceSeries(i))
},
selectedBooks (val: BookDto[]) {
selectedBooks(val: BookDto[]) {
val.forEach(i => this.replaceBook(i))
},
},
beforeRouteUpdate(to, from, next) {
if (to.params.libraryId !== from.params.libraryId) {
this.loadAll(to.params.libraryId)
}
next()
},
computed: {
fixedCardWidth (): number {
fixedCardWidth(): number {
return this.$vuetify.breakpoint.name === 'xs' ? 120 : 150
},
allEmpty (): boolean {
allEmpty(): boolean {
return this.newSeries.length === 0 &&
this.updatedSeries.length === 0 &&
this.latestBooks.length === 0 &&
this.inProgressBooks.length === 0 &&
this.onDeckBooks.length === 0
},
showLibraryBar(): boolean {
return this.libraryId !== LIBRARIES_ALL
},
},
methods: {
libraryDeleted () {
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' })
this.$router.push({name: 'welcome'})
} else {
this.loadAll()
this.reload()
}
},
loadAll () {
reload() {
this.loadAll(this.libraryId)
},
loadAll(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
this.selectedSeries = []
this.selectedBooks = []
this.loadNewSeries()
this.loadUpdatedSeries()
this.loadLatestBooks()
this.loadInProgressBooks()
this.loadOnDeckBooks()
this.loadNewSeries(libraryId)
this.loadUpdatedSeries(libraryId)
this.loadLatestBooks(libraryId)
this.loadInProgressBooks(libraryId)
this.loadOnDeckBooks(libraryId)
},
replaceSeries (series: SeriesDto) {
replaceSeries(series: SeriesDto) {
let index = this.newSeries.findIndex(x => x.id === series.id)
if (index !== -1) {
this.newSeries.splice(index, 1, series)
@ -196,7 +247,7 @@ export default Vue.extend({
this.updatedSeries.splice(index, 1, series)
}
},
replaceBook (book: BookDto) {
replaceBook(book: BookDto) {
let index = this.latestBooks.findIndex(x => x.id === book.id)
if (index !== -1) {
this.latestBooks.splice(index, 1, book)
@ -210,36 +261,36 @@ export default Vue.extend({
this.onDeckBooks.splice(index, 1, book)
}
},
async loadNewSeries () {
this.newSeries = (await this.$komgaSeries.getNewSeries()).content
async loadNewSeries(libraryId: string) {
this.newSeries = (await this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId))).content
},
async loadUpdatedSeries () {
this.updatedSeries = (await this.$komgaSeries.getUpdatedSeries()).content
async loadUpdatedSeries(libraryId: string) {
this.updatedSeries = (await this.$komgaSeries.getUpdatedSeries(this.getRequestLibraryId(libraryId))).content
},
async loadLatestBooks () {
async loadLatestBooks(libraryId: string) {
const pageRequest = {
sort: ['createdDate,desc'],
} as PageRequest
this.latestBooks = (await this.$komgaBooks.getBooks(undefined, pageRequest)).content
this.latestBooks = (await this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageRequest)).content
},
async loadInProgressBooks () {
async loadInProgressBooks(libraryId: string) {
const pageRequest = {
sort: ['readProgress.lastModified,desc'],
} as PageRequest
this.inProgressBooks = (await this.$komgaBooks.getBooks(undefined, pageRequest, undefined, undefined, [ReadStatus.IN_PROGRESS])).content
this.inProgressBooks = (await this.$komgaBooks.getBooks(this.getRequestLibraryId(libraryId), pageRequest, undefined, undefined, [ReadStatus.IN_PROGRESS])).content
},
async loadOnDeckBooks () {
this.onDeckBooks = (await this.$komgaBooks.getBooksOnDeck()).content
async loadOnDeckBooks(libraryId: string) {
this.onDeckBooks = (await this.$komgaBooks.getBooksOnDeck(this.getRequestLibraryId(libraryId))).content
},
singleEditSeries (series: SeriesDto) {
singleEditSeries(series: SeriesDto) {
this.$store.dispatch('dialogUpdateSeries', series)
},
singleEditBook (book: BookDto) {
singleEditBook(book: BookDto) {
this.$store.dispatch('dialogUpdateBooks', book)
},
async markSelectedSeriesRead () {
async markSelectedSeriesRead() {
await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.markAsRead(s.id),
))
@ -247,7 +298,7 @@ export default Vue.extend({
this.$komgaSeries.getOneSeries(s.id),
))
},
async markSelectedSeriesUnread () {
async markSelectedSeriesUnread() {
await Promise.all(this.selectedSeries.map(s =>
this.$komgaSeries.markAsUnread(s.id),
))
@ -255,27 +306,27 @@ export default Vue.extend({
this.$komgaSeries.getOneSeries(s.id),
))
},
addToCollection () {
addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', this.selectedSeries)
},
editMultipleSeries () {
editMultipleSeries() {
this.$store.dispatch('dialogUpdateSeries', this.selectedSeries)
},
editMultipleBooks () {
editMultipleBooks() {
this.$store.dispatch('dialogUpdateBooks', this.selectedBooks)
},
addToReadList () {
addToReadList() {
this.$store.dispatch('dialogAddBooksToReadList', this.selectedBooks)
},
async markSelectedBooksRead () {
async markSelectedBooksRead() {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.updateReadProgress(b.id, { completed: true }),
this.$komgaBooks.updateReadProgress(b.id, {completed: true}),
))
this.selectedBooks = await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.getBook(b.id),
))
},
async markSelectedBooksUnread () {
async markSelectedBooksUnread() {
await Promise.all(this.selectedBooks.map(b =>
this.$komgaBooks.deleteReadProgress(b.id),
))
@ -283,6 +334,13 @@ export default Vue.extend({
this.$komgaBooks.getBook(b.id),
))
},
getLibraryLazy(libraryId: string): LibraryDto | undefined {
if (libraryId !== LIBRARIES_ALL) {
return this.$store.getters.getLibraryById(libraryId)
} else {
return undefined
}
},
},
})
</script>

View file

@ -25,7 +25,7 @@
<v-divider/>
<v-list>
<v-list-item :to="{name: 'home'}" exact>
<v-list-item :to="{name: 'dashboard'}">
<v-list-item-icon>
<v-icon>mdi-home</v-icon>
</v-list-item-icon>
@ -34,7 +34,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item :to="{name:'browse-libraries', params: {libraryId: 'all'}}">
<v-list-item :to="{name:'libraries', params: {libraryId: LIBRARIES_ALL}}">
<v-list-item-icon>
<v-icon>mdi-book-multiple</v-icon>
</v-list-item-icon>
@ -51,7 +51,7 @@
<v-list-item v-for="(l, index) in libraries"
:key="index"
dense
:to="{name:'browse-libraries', params: {libraryId: l.id}}"
:to="{name:'libraries', params: {libraryId: l.id}}"
>
<v-list-item-icon>
</v-list-item-icon>
@ -158,12 +158,14 @@ 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";
export default Vue.extend({
name: 'home',
components: {LibraryActionsMenu, SearchBox, Dialogs},
data: function () {
return {
LIBRARIES_ALL,
drawerVisible: this.$vuetify.breakpoint.lgAndUp,
info: {} as ActuatorInfo,
locales: this.$i18n.availableLocales.map((x: any) => ({text: this.$i18n.t('common.locale_name', x), value: x})),

View file

@ -144,14 +144,12 @@ class BookDtoDao(
): BookDto? =
findSiblingReadList(readListId, bookId, userId, filterOnLibraryIds, next = true)
override fun findOnDeck(libraryIds: Collection<String>, userId: String, pageable: Pageable): Page<BookDto> {
val conditions = if (libraryIds.isEmpty()) DSL.trueCondition() else s.LIBRARY_ID.`in`(libraryIds)
override fun findOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<BookDto> {
val seriesIds = dsl.select(s.ID)
.from(s)
.leftJoin(b).on(s.ID.eq(b.SERIES_ID))
.leftJoin(r).on(b.ID.eq(r.BOOK_ID)).and(readProgressCondition(userId))
.where(conditions)
.apply { filterOnLibraryIds?.let { where(s.LIBRARY_ID.`in`(it)) } }
.groupBy(s.ID)
.having(SeriesDtoDao.countUnread.ge(inline(1.toBigDecimal())))
.and(SeriesDtoDao.countRead.ge(inline(1.toBigDecimal())))

View file

@ -153,16 +153,14 @@ class BookController(
@GetMapping("api/v1/books/ondeck")
fun getBooksOnDeck(
@AuthenticationPrincipal principal: KomgaPrincipal,
@RequestParam(name = "library_id", required = false) libraryIds: List<String>?,
@Parameter(hidden = true) page: Pageable
): Page<BookDto> {
val libraryIds = if (principal.user.sharedAllLibraries) emptySet() else principal.user.sharedLibrariesIds
return bookDtoRepository.findOnDeck(
libraryIds,
): Page<BookDto> =
bookDtoRepository.findOnDeck(
principal.user.id,
principal.user.getAuthorizedLibraryIds(libraryIds),
page
).map { it.restrictUrl(!principal.user.roleAdmin) }
}
@GetMapping("api/v1/books/{bookId}")
fun getOneBook(
@ -289,7 +287,10 @@ class BookController(
val media = mediaRepository.findById(book.id)
when (media.status) {
Media.Status.UNKNOWN -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book has not been analyzed yet")
Media.Status.OUTDATED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book is outdated and must be re-analyzed")
Media.Status.OUTDATED -> throw ResponseStatusException(
HttpStatus.NOT_FOUND,
"Book is outdated and must be re-analyzed"
)
Media.Status.ERROR -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
Media.Status.UNSUPPORTED -> throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book format is not supported")
Media.Status.READY -> media.pages.mapIndexed { index, s ->
@ -319,7 +320,10 @@ class BookController(
request: WebRequest,
@PathVariable bookId: String,
@PathVariable pageNumber: Int,
@Parameter(description = "Convert the image to the provided format.", schema = Schema(allowableValues = ["jpeg", "png"]))
@Parameter(
description = "Convert the image to the provided format.",
schema = Schema(allowableValues = ["jpeg", "png"])
)
@RequestParam(value = "convert", required = false) convertTo: String?,
@Parameter(description = "If set to true, pages will start at index 0. If set to false, pages will start at index 1.")
@RequestParam(value = "zero_based", defaultValue = "false") zeroBasedIndex: Boolean

View file

@ -37,5 +37,5 @@ interface BookDtoRepository {
filterOnLibraryIds: Collection<String>?
): BookDto?
fun findOnDeck(libraryIds: Collection<String>, userId: String, pageable: Pageable): Page<BookDto>
fun findOnDeck(userId: String, filterOnLibraryIds: Collection<String>?, pageable: Pageable): Page<BookDto>
}

View file

@ -231,8 +231,8 @@ class BookDtoDaoTest(
// when
val found = bookDtoDao.findOnDeck(
emptyList(),
user.id,
null,
PageRequest.of(0, 20)
)
@ -252,8 +252,8 @@ class BookDtoDaoTest(
// when
val found = bookDtoDao.findOnDeck(
emptyList(),
user.id,
null,
PageRequest.of(0, 20)
)
@ -276,8 +276,8 @@ class BookDtoDaoTest(
// when
val found = bookDtoDao.findOnDeck(
emptyList(),
user.id,
null,
PageRequest.of(0, 20)
)