feat(webui): add ability to pin/unpin libraries

Closes: #1560
This commit is contained in:
Gauthier Roebroeck 2025-02-06 15:38:31 +08:00
parent 4892945ddf
commit c8e4a462a2
25 changed files with 311 additions and 82 deletions

View file

@ -80,6 +80,7 @@
import Vue from 'vue'
import {COLLECTION_ADDED, COLLECTION_DELETED, READLIST_ADDED, READLIST_DELETED} from '@/types/events'
import {LIBRARIES_ALL} from '@/types/library'
import {LibraryDto} from '@/types/komga-libraries'
export default Vue.extend({
name: 'LibraryNavigation',
@ -107,6 +108,14 @@ export default Vue.extend({
},
immediate: true,
},
'$store.getters.getLibrariesPinned': {
handler(val) {
if (this.libraryId === LIBRARIES_ALL) {
this.loadCollectionCounts(this.libraryId)
this.loadReadListCounts(this.libraryId)
}
},
},
},
created() {
this.$eventHub.$on(COLLECTION_ADDED, this.collectionAdded)
@ -142,12 +151,12 @@ export default Vue.extend({
if(this.collectionsCount === 1) this.loadCollectionCounts(this.libraryId)
},
async loadCollectionCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
this.$komgaCollections.getCollections(lib, {size: 0})
.then(v => this.collectionsCount = v.totalElements)
},
async loadReadListCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
await this.$komgaReadLists.getReadLists(lib, {size: 0})
.then(v => this.readListsCount = v.totalElements)
},

View file

@ -100,7 +100,6 @@
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR, ErrorEvent} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import ThumbnailCard from '@/components/ThumbnailCard.vue'
import DropZone from '@/components/DropZone.vue'
@ -152,9 +151,6 @@ export default Vue.extend({
},
},
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
getErrorsName(): string {
if (this.form.name === '') return this.$t('common.required').toString()
if (this.form.name?.toLowerCase() !== this.collection.name?.toLowerCase() && this.collections.some(e => e.name.toLowerCase() === this.form.name.toLowerCase())) {

View file

@ -113,7 +113,6 @@
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR, ErrorEvent} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import DropZone from '@/components/DropZone.vue'
import ThumbnailCard from '@/components/ThumbnailCard.vue'
import {ReadListDto, ReadListThumbnailDto, ReadListUpdateDto} from '@/types/komga-readlists'
@ -167,9 +166,6 @@ export default Vue.extend({
},
},
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
getErrorsName(): string {
if (this.form.name === '') return this.$t('common.required').toString()
if (this.form.name?.toLowerCase() !== this.readList.name?.toLowerCase() && this.readLists.some(e => e.name.toLowerCase() === this.form.name.toLowerCase())) {

View file

@ -43,7 +43,6 @@
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import {UserDto, UserUpdateDto} from '@/types/komga-users'
export default Vue.extend({
@ -79,9 +78,6 @@ export default Vue.extend({
value: x,
}))
},
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
},
methods: {
dialogReset(user: UserDto) {

View file

@ -188,7 +188,7 @@ export default Vue.extend({
},
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
return this.$store.getters.getLibraries
},
ageRestrictionsAvailable(): any[] {
return [

View file

@ -1,32 +1,40 @@
<template>
<div>
<v-menu offset-y v-if="isAdmin">
<v-menu offset-y>
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item @click="scan(false)">
<v-list-item v-if="!library.unpinned" @click="unpin">
<v-list-item-title>{{ $t('menu.unpin') }}</v-list-item-title>
</v-list-item>
<v-list-item v-if="library.unpinned" @click="pin">
<v-list-item-title>{{ $t('menu.pin') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="scan(false)" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.scan_library_files') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="scan(true)" class="list-warning">
<v-list-item @click="scan(true)" class="list-warning" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.scan_library_files_deep') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmAnalyzeModal = true">
<v-list-item @click="confirmAnalyzeModal = true" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.analyze') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmRefreshMetadataModal = true">
<v-list-item @click="confirmRefreshMetadataModal = true" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.refresh_metadata') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmEmptyTrash = true">
<v-list-item @click="confirmEmptyTrash = true" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.empty_trash') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="edit">
<v-list-item @click="edit" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.edit') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="promptDeleteLibrary"
class="list-danger">
class="list-danger"
v-if="isAdmin"
>
<v-list-item-title>{{ $t('menu.delete') }}</v-list-item-title>
</v-list-item>
</v-list>
@ -61,6 +69,7 @@
import Vue from 'vue'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import {LibraryDto} from '@/types/komga-libraries'
import {ClientSettingLibraryUpdate} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'LibraryActionsMenu',
@ -84,6 +93,22 @@ export default Vue.extend({
},
},
methods: {
unpin() {
this.$store.dispatch('updateLibrarySetting', {
libraryId: this.library.id,
patch: {
unpinned: true,
},
} as ClientSettingLibraryUpdate)
},
pin() {
this.$store.dispatch('updateLibrarySetting', {
libraryId: this.library.id,
patch: {
unpinned: false,
},
} as ClientSettingLibraryUpdate)
},
scan(scanDeep: boolean) {
this.$komgaLibraries.scanLibrary(this.library, scanDeep)
},

View file

@ -248,6 +248,7 @@
"locale_rtl": "false",
"lock_all": "Lock all",
"media": "Media",
"more": "More",
"n_selected": "{count} selected",
"nothing_to_show": "Nothing to show",
"ok": "OK",
@ -261,6 +262,7 @@
"password": "Password",
"pdf": "PDF",
"pending_tasks": "No pending tasks | 1 pending task | {count} pending tasks",
"pinned_libraries": "Pinned Libraries",
"publisher": "Publisher",
"read": "Read",
"read_on": "Read on {date}",
@ -892,10 +894,12 @@
"empty_trash": "Empty trash",
"mark_read": "Mark as read",
"mark_unread": "Mark as unread",
"pin": "Pin",
"refresh_metadata": "Refresh metadata",
"scan_library_files": "Scan library files",
"scan_library_files_deep": "Scan library files (deep)",
"select_all": "Select all"
"select_all": "Select all",
"unpin": "Unpin"
},
"metrics": {
"library_books": "Books per library",
@ -914,6 +918,10 @@
"libraries": "Libraries",
"logout": "Log Out"
},
"no_libraries_pinned": {
"title": "No pinned libraries",
"subtitle": "You can pin a library from the 3-dots menu"
},
"page_not_found": {
"go_back_to_home_page": "Go back to home page",
"page_does_not_exist": "The page you are looking for doesn't exist.",

View file

@ -59,6 +59,7 @@ Vue.use(Chartkick.use(Chart))
Vue.use(httpPlugin)
Vue.use(logger)
Vue.use(komgaSettings, {store: store, http: Vue.prototype.$http})
Vue.use(komgaFileSystem, {http: Vue.prototype.$http})
Vue.use(komgaSeries, {http: Vue.prototype.$http})
Vue.use(komgaCollections, {http: Vue.prototype.$http})
@ -80,7 +81,6 @@ Vue.use(komgaMetrics, {http: Vue.prototype.$http})
Vue.use(komgaHistory, {http: Vue.prototype.$http})
Vue.use(komgaAnnouncements, {http: Vue.prototype.$http})
Vue.use(komgaReleases, {http: Vue.prototype.$http})
Vue.use(komgaSettings, {store: store, http: Vue.prototype.$http})
Vue.use(komgaFonts, {http: Vue.prototype.$http})
Vue.config.productionTip = false

View file

@ -11,35 +11,47 @@ const vuexModule: Module<any, any> = {
libraries: [] as LibraryDto[],
},
getters: {
getLibraryById: (state) => (id: number) => {
return state.libraries.find((l: any) => l.id === id)
getLibraries(state, getters) {
const settings = getters.getClientSettingsLibraries
return state.libraries
.map((it: LibraryDto) => Object.assign({}, it, settings[it.id]))
.sort((a: LibraryDto, b: LibraryDto) => a.name.toLowerCase() > b.name.toLowerCase())
},
getLibraryById: (state, getters) => (id: number) => {
return getters.getLibraries.find((l: any) => l.id === id)
},
getLibrariesPinned(state, getters) {
return getters.getLibraries.filter((it: LibraryDto) => !it.unpinned)
},
getLibrariesUnpinned(state, getters) {
return getters.getLibraries.filter((it: LibraryDto) => it.unpinned)
},
},
mutations: {
setLibraries (state, libraries) {
setLibraries(state, libraries) {
state.libraries = libraries
},
},
actions: {
async getLibraries ({ commit }) {
async getLibraries({commit}) {
commit('setLibraries', await service.getLibraries())
},
async postLibrary ({ dispatch }, library) {
async postLibrary({dispatch}, library) {
await service.postLibrary(library)
},
async updateLibrary ({ dispatch }, { libraryId, library }) {
async updateLibrary({dispatch}, {libraryId, library}) {
await service.updateLibrary(libraryId, library)
},
async deleteLibrary ({ dispatch }, library) {
async deleteLibrary({dispatch}, library) {
await service.deleteLibrary(library)
},
},
}
export default {
install (
install(
Vue: typeof _Vue,
{ store, http }: { store: any, http: AxiosInstance }) {
{store, http}: { store: any, http: AxiosInstance }) {
service = new KomgaLibrariesService(http)
Vue.prototype.$komgaLibraries = service

View file

@ -2,7 +2,13 @@ import {AxiosInstance} from 'axios'
import _Vue from 'vue'
import KomgaSettingsService from '@/services/komga-settings.service'
import {Module} from 'vuex'
import {ClientSettingDto} from '@/types/komga-clientsettings'
import {
CLIENT_SETTING,
ClientSettingDto,
ClientSettingLibrary,
ClientSettingLibraryUpdate,
ClientSettingUserUpdateDto,
} from '@/types/komga-clientsettings'
let service: KomgaSettingsService
@ -15,6 +21,14 @@ const vuexModule: Module<any, any> = {
getClientSettings(state): Record<string, ClientSettingDto> {
return {...state.clientSettingsGlobal, ...state.clientSettingsUser}
},
getClientSettingsLibraries(state): Record<string, ClientSettingLibrary> {
let settings: Record<string, ClientSettingLibrary> = {}
try {
settings = JSON.parse(state.clientSettingsUser[CLIENT_SETTING.WEBUI_LIBRARIES]?.value)
} catch (e) {
}
return settings
},
},
mutations: {
setClientSettingsGlobal(state, settings) {
@ -31,6 +45,16 @@ const vuexModule: Module<any, any> = {
async getClientSettingsUser({commit}) {
commit('setClientSettingsUser', await service.getClientSettingsUser())
},
async updateLibrarySetting({dispatch, getters}, update: ClientSettingLibraryUpdate) {
const all = getters.getClientSettingsLibraries
all[update.libraryId] = Object.assign({}, all[update.libraryId], update.patch)
const newSettings = {} as Record<string, ClientSettingUserUpdateDto>
newSettings[CLIENT_SETTING.WEBUI_LIBRARIES] = {
value: JSON.stringify(all),
}
await service.updateClientSettingUser(newSettings)
dispatch('getClientSettingsUser')
},
},
}

View file

@ -21,6 +21,14 @@ const noLibraryGuard = (to: any, from: any, next: any) => {
} else next()
}
const noLibraryNorPinGuard = (to: any, from: any, next: any) => {
if (lStore.state.komgaLibraries.libraries.length === 0) {
next({name: 'welcome'})
} else if (lStore.getters.getLibrariesPinned.length === 0) {
next({name: 'no-pins'})
} else next()
}
const getLibraryRoute = (libraryId: string) => {
switch ((lStore.getters.getLibraryRoute(libraryId) as LIBRARY_ROUTE)) {
case LIBRARY_ROUTE.COLLECTIONS:
@ -57,10 +65,15 @@ const router = new Router({
name: 'welcome',
component: () => import(/* webpackChunkName: "welcome" */ './views/WelcomeView.vue'),
},
{
path: '/no-pins',
name: 'no-pins',
component: () => import(/* webpackChunkName: "no-pins" */ './views/NoPinnedLibraries.vue'),
},
{
path: '/dashboard',
name: 'dashboard',
beforeEnter: noLibraryGuard,
beforeEnter: noLibraryNorPinGuard,
component: () => import(/* webpackChunkName: "dashboard" */ './views/DashboardView.vue'),
},
{

View file

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

View file

@ -1,5 +1,4 @@
import {AxiosInstance} from 'axios'
import {AuthorDto, BookDto} from '@/types/komga-books'
import {GroupCountDto, SeriesDto, SeriesMetadataUpdateDto, SeriesThumbnailDto} from '@/types/komga-series'
import {SeriesSearch} from '@/types/komga-search'
@ -41,13 +40,14 @@ export default class KomgaSeriesService {
}
}
async getNewSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
async getNewSeries(libraryIds?: string[], oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = {...pageRequest} as any
if (libraryId) params.library_id = libraryId
if (libraryIds) params.library_id = libraryIds
if (oneshot !== undefined) params.oneshot = oneshot
return (await this.http.get(`${API_SERIES}/new`, {
params: params,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve new series'
@ -58,13 +58,14 @@ export default class KomgaSeriesService {
}
}
async getUpdatedSeries(libraryId?: string, oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
async getUpdatedSeries(libraryIds?: string[], oneshot?: boolean, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
const params = {...pageRequest} as any
if (libraryId) params.library_id = libraryId
if (libraryIds) params.library_id = libraryIds
if (oneshot !== undefined) params.oneshot = oneshot
return (await this.http.get(`${API_SERIES}/updated`, {
params: params,
paramsSerializer: params => qs.stringify(params, {indices: false}),
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve updated series'

View file

@ -17,4 +17,15 @@ export enum CLIENT_SETTING {
WEBUI_OAUTH2_AUTO_LOGIN = 'webui.oauth2.auto_login',
WEBUI_POSTER_STRETCH = 'webui.poster.stretch',
WEBUI_POSTER_BLUR_UNREAD = 'webui.poster.blur_unread',
WEBUI_LIBRARIES = 'webui.libraries',
}
export interface ClientSettingLibrary {
unpinned?: boolean,
order?: number,
}
export interface ClientSettingLibraryUpdate {
libraryId: string,
patch: ClientSettingLibrary,
}

View file

@ -31,6 +31,10 @@ export interface LibraryDto {
analyzeDimensions: boolean,
oneshotsDirectory: string,
unavailable: boolean,
// custom fields
unpinned: boolean,
order: number,
}
export interface LibraryCreationDto {

View file

@ -341,7 +341,7 @@ export default Vue.extend({
},
async resetParams(route: any, collectionId: string) {
// load dynamic filters
this.$set(this.filterOptions, 'library', this.$store.state.komgaLibraries.libraries.map((x: LibraryDto) => ({
this.$set(this.filterOptions, 'library', this.$store.getters.getLibraries.map((x: LibraryDto) => ({
name: x.name,
value: x.id,
})))

View file

@ -7,7 +7,7 @@
<libraries-actions-menu v-else/>
<v-toolbar-title>
<span>{{ library ? library.name : $t('common.all_libraries') }}</span>
<span>{{ toolbarTitle }}</span>
<v-chip label class="mx-4" v-if="totalElements">
<span style="font-size: 1.1rem">{{ totalElements }}</span>
</v-chip>
@ -87,7 +87,6 @@ export default Vue.extend({
},
data: () => {
return {
library: undefined as LibraryDto | undefined,
collections: [] as CollectionDto[],
selectedCollections: [] as CollectionDto[],
page: 1,
@ -104,6 +103,14 @@ export default Vue.extend({
default: LIBRARIES_ALL,
},
},
watch: {
'$store.getters.getLibrariesPinned': {
handler(val) {
if (this.libraryId === LIBRARIES_ALL)
this.loadLibrary(this.libraryId)
},
},
},
created() {
this.$eventHub.$on(COLLECTION_ADDED, this.reloadCollections)
this.$eventHub.$on(COLLECTION_CHANGED, this.reloadCollections)
@ -142,6 +149,14 @@ export default Vue.extend({
next()
},
computed: {
library(): LibraryDto | undefined {
return this.getLibraryLazy(this.libraryId)
},
toolbarTitle(): string {
if (this.library) return this.library.name
else if (this.$store.getters.getLibrariesPinned.length > 0) return this.$t('common.pinned_libraries').toString()
else return this.$t('common.all_libraries').toString()
},
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
@ -205,7 +220,6 @@ export default Vue.extend({
}
},
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
if (this.library != undefined) document.title = `Komga - ${this.library.name}`
await this.loadPage(libraryId, this.page)
@ -221,7 +235,7 @@ export default Vue.extend({
size: this.pageSize,
} as PageRequest
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map(it => it.id)
const collectionsPage = await this.$komgaCollections.getCollections(lib, pageRequest)
this.totalPages = collectionsPage.totalPages

View file

@ -7,7 +7,7 @@
<libraries-actions-menu v-else/>
<v-toolbar-title>
<span>{{ library ? library.name : $t('common.all_libraries') }}</span>
<span>{{ toolbarTitle }}</span>
<v-chip label class="mx-4" v-if="totalElements">
<span style="font-size: 1.1rem">{{ totalElements }}</span>
</v-chip>
@ -168,7 +168,8 @@ import {ItemContext} from '@/types/items'
import {
BookSearch,
SearchConditionAgeRating,
SearchConditionAllOfSeries, SearchConditionAnyOfBook,
SearchConditionAllOfSeries,
SearchConditionAnyOfBook,
SearchConditionAnyOfSeries,
SearchConditionAuthor,
SearchConditionComplete,
@ -229,7 +230,6 @@ export default Vue.extend({
},
data: function () {
return {
library: undefined as LibraryDto | undefined,
series: [] as SeriesDto[],
seriesGroups: [] as GroupCountDto[],
alphabeticalNavigation: ['ALL', '#', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'],
@ -266,6 +266,14 @@ export default Vue.extend({
default: LIBRARIES_ALL,
},
},
watch: {
'$store.getters.getLibrariesPinned': {
handler(val) {
if (this.libraryId === LIBRARIES_ALL)
this.loadLibrary(this.libraryId)
},
},
},
created() {
this.$eventHub.$on(SERIES_ADDED, this.seriesChanged)
this.$eventHub.$on(SERIES_CHANGED, this.seriesChanged)
@ -319,6 +327,14 @@ export default Vue.extend({
next()
},
computed: {
library(): LibraryDto | undefined {
return this.getLibraryLazy(this.libraryId)
},
toolbarTitle(): string {
if (this.library) return this.library.name
else if (this.$store.getters.getLibrariesPinned.length > 0) return this.$t('common.pinned_libraries').toString()
else return this.$t('common.all_libraries').toString()
},
symbolCondition(): SearchConditionSeries | undefined {
if (this.selectedSymbol === 'ALL') return undefined
if (this.selectedSymbol === '#') return new SearchConditionAllOfSeries(
@ -633,7 +649,6 @@ export default Vue.extend({
if (this.series.some(b => b.id === event.seriesId)) this.reloadPage()
},
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
if (this.library != undefined) document.title = `Komga - ${this.library.name}`
await this.loadPage(libraryId, this.page, this.sortActive, this.symbolCondition)
@ -671,6 +686,11 @@ export default Vue.extend({
const conditions = [] as SearchConditionSeries[]
if (libraryId !== LIBRARIES_ALL) conditions.push(new SearchConditionLibraryId(new SearchOperatorIs(libraryId)))
else {
conditions.push(new SearchConditionAnyOfSeries(
this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => new SearchConditionLibraryId(new SearchOperatorIs(it.id))),
))
}
if (this.filters.status && this.filters.status.length > 0) this.filtersMode?.status?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.status)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.status))
if (this.filters.readStatus && this.filters.readStatus.length > 0) conditions.push(new SearchConditionAnyOfSeries(this.filters.readStatus))
if (this.filters.genre && this.filters.genre.length > 0) this.filtersMode?.genre?.allOf ? conditions.push(new SearchConditionAllOfSeries(this.filters.genre)) : conditions.push(new SearchConditionAnyOfSeries(this.filters.genre))

View file

@ -355,7 +355,7 @@ export default Vue.extend({
},
async resetParams(route: any, readListId: string) {
// load dynamic filters
this.$set(this.filterOptions, 'library', this.$store.state.komgaLibraries.libraries.map((x: LibraryDto) => ({
this.$set(this.filterOptions, 'library', this.$store.getters.getLibraries.map((x: LibraryDto) => ({
name: x.name,
value: x.id,
})))

View file

@ -7,7 +7,7 @@
<libraries-actions-menu v-else/>
<v-toolbar-title>
<span>{{ library ? library.name : $t('common.all_libraries') }}</span>
<span>{{ toolbarTitle }}</span>
<v-chip label class="mx-4" v-if="totalElements">
<span style="font-size: 1.1rem">{{ totalElements }}</span>
</v-chip>
@ -88,7 +88,6 @@ export default Vue.extend({
},
data: () => {
return {
library: undefined as LibraryDto | undefined,
readLists: [] as ReadListDto[],
selectedReadLists: [] as ReadListDto[],
page: 1,
@ -105,6 +104,14 @@ export default Vue.extend({
default: LIBRARIES_ALL,
},
},
watch: {
'$store.getters.getLibrariesPinned': {
handler(val) {
if (this.libraryId === LIBRARIES_ALL)
this.loadLibrary(this.libraryId)
},
},
},
created() {
this.$eventHub.$on(READLIST_ADDED, this.reloadElements)
this.$eventHub.$on(READLIST_CHANGED, this.reloadElements)
@ -143,6 +150,14 @@ export default Vue.extend({
next()
},
computed: {
library(): LibraryDto | undefined {
return this.getLibraryLazy(this.libraryId)
},
toolbarTitle(): string {
if (this.library) return this.library.name
else if (this.$store.getters.getLibrariesPinned.length > 0) return this.$t('common.pinned_libraries').toString()
else return this.$t('common.all_libraries').toString()
},
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
@ -206,7 +221,6 @@ export default Vue.extend({
}
},
async loadLibrary(libraryId: string) {
this.library = this.getLibraryLazy(libraryId)
if (this.library != undefined) document.title = `Komga - ${this.library.name}`
await this.loadPage(libraryId, this.page)
@ -222,7 +236,7 @@ export default Vue.extend({
size: this.pageSize,
} as PageRequest
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map(it => it.id)
const elementsPage = await this.$komgaReadLists.getReadLists(lib, pageRequest)
this.totalPages = elementsPage.totalPages

View file

@ -235,10 +235,15 @@ import {PageLoader} from '@/types/pageLoader'
import {ItemContext} from '@/types/items'
import {
BookSearch,
SearchConditionAllOfBook, SearchConditionAnyOfBook,
SearchConditionAllOfBook,
SearchConditionAnyOfBook,
SearchConditionAnyOfSeries,
SearchConditionBook,
SearchConditionLibraryId,
SearchConditionReadStatus, SearchConditionReleaseDate, SearchConditionSeriesId, SearchOperatorAfter,
SearchConditionReadStatus,
SearchConditionReleaseDate,
SearchConditionSeriesId,
SearchOperatorAfter,
SearchOperatorIs,
} from '@/types/komga-search'
@ -257,7 +262,6 @@ export default Vue.extend({
return {
ItemContext,
loading: false,
library: undefined as LibraryDto | undefined,
loaderNewSeries: undefined as unknown as PageLoader<SeriesDto>,
loaderUpdatedSeries: undefined as unknown as PageLoader<SeriesDto>,
loaderLatestBooks: undefined as unknown as PageLoader<BookDto>,
@ -299,7 +303,7 @@ export default Vue.extend({
route: LIBRARY_ROUTE.RECOMMENDED,
})
this.setupLoaders(this.libraryId)
this.loadAll(this.libraryId)
this.loadAll()
},
props: {
libraryId: {
@ -310,7 +314,11 @@ export default Vue.extend({
watch: {
libraryId(val) {
this.setupLoaders(val)
this.loadAll(val)
this.loadAll()
},
libraryIds() {
this.setupLoaders(this.libraryId)
this.loadAll()
},
'$store.state.komgaLibraries.libraries': {
handler(val) {
@ -318,8 +326,19 @@ export default Vue.extend({
else this.reload()
},
},
'$store.getters.getLibrariesPinned': {
handler(val) {
if (val.length === 0) this.$router.push({name: 'no-pins'})
},
},
},
computed: {
library(): LibraryDto | undefined {
return this.getLibraryLazy(this.libraryId)
},
libraryIds(): string[] {
return this.libraryId !== LIBRARIES_ALL ? [this.libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
},
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
@ -346,8 +365,8 @@ export default Vue.extend({
async scrollChanged(loader: PageLoader<any>, percent: number) {
if (percent > 0.95) await loader.loadNext()
},
getRequestLibraryId(libraryId: string): string | undefined {
return libraryId !== LIBRARIES_ALL ? libraryId : undefined
getRequestLibraryId(libraryId: string): string[] {
return libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
},
seriesChanged(event: SeriesSseDto) {
if (this.libraryId === LIBRARIES_ALL || event.libraryId === this.libraryId) {
@ -371,11 +390,15 @@ export default Vue.extend({
else if (this.loaderNewSeries?.items.some(s => s.id === event.seriesId)) this.reload()
},
reload: throttle(function (this: any) {
this.loadAll(this.libraryId, true)
this.loadAll(true)
}, 5000),
setupLoaders(libraryId: string) {
const requestLibraries = this.getRequestLibraryId(libraryId)
const baseBookConditions = [] as SearchConditionBook[]
if (libraryId !== LIBRARIES_ALL) baseBookConditions.push(new SearchConditionLibraryId(new SearchOperatorIs(libraryId)))
if (requestLibraries)
baseBookConditions.push(new SearchConditionAnyOfSeries(
requestLibraries.map((it: string) => new SearchConditionLibraryId(new SearchOperatorIs(it))),
))
this.loaderInProgressBooks = new PageLoader<BookDto>(
{sort: ['readProgress.readDate,desc']},
@ -385,7 +408,7 @@ export default Vue.extend({
)
this.loaderOnDeckBooks = new PageLoader<BookDto>(
{},
(pageable: PageRequest) => this.$komgaBooks.getBooksOnDeck(this.getRequestLibraryId(libraryId), pageable),
(pageable: PageRequest) => this.$komgaBooks.getBooksOnDeck(requestLibraries, pageable),
)
this.loaderLatestBooks = new PageLoader<BookDto>(
{sort: ['createdDate,desc']},
@ -408,16 +431,15 @@ export default Vue.extend({
this.loaderNewSeries = new PageLoader<SeriesDto>(
{},
(pageable: PageRequest) => this.$komgaSeries.getNewSeries(this.getRequestLibraryId(libraryId), false, pageable),
(pageable: PageRequest) => this.$komgaSeries.getNewSeries(requestLibraries, false, pageable),
)
this.loaderUpdatedSeries = new PageLoader<SeriesDto>(
{},
(pageable: PageRequest) => this.$komgaSeries.getUpdatedSeries(this.getRequestLibraryId(libraryId), false, pageable),
(pageable: PageRequest) => this.$komgaSeries.getUpdatedSeries(requestLibraries, false, pageable),
)
},
loadAll(libraryId: string, reload: boolean = false) {
loadAll(reload: boolean = false) {
this.loading = true
this.library = this.getLibraryLazy(libraryId)
if (this.library != undefined) document.title = `Komga - ${this.library.name}`
this.selectedSeries = []
this.selectedBooks = []

View file

@ -80,7 +80,8 @@
</v-list-item-action>
</v-list-item>
<v-list-item v-for="(l, index) in libraries"
<!-- PINNED LIBRARIES -->
<v-list-item v-for="(l, index) in librariesPinned"
:key="index"
:to="{name:'libraries', params: {libraryId: l.id}}"
>
@ -94,11 +95,38 @@
>{{ $t('common.unavailable') }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action v-if="isAdmin" class="ma-0">
<v-list-item-action class="ma-0">
<library-actions-menu :library="l"/>
</v-list-item-action>
</v-list-item>
<!-- UNPINNED LIBRARIES -->
<v-list-group no-action
sub-group
v-if="librariesUnpinned.length > 0"
>
<template v-slot:activator>
<v-list-item-title>{{ $t('common.more') }}</v-list-item-title>
</template>
<v-list-item v-for="(l, index) in librariesUnpinned"
:key="index"
:to="{name:'libraries', params: {libraryId: l.id}}"
>
<v-list-item-content>
<v-list-item-title>{{ l.name }}</v-list-item-title>
<v-list-item-subtitle
v-if="l.unavailable"
class="error--text caption"
>{{ $t('common.unavailable') }}
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action class="ma-0">
<library-actions-menu :library="l"/>
</v-list-item-action>
</v-list-item>
</v-list-group>
<!-- IMPORT -->
<v-list-group v-if="isAdmin"
prepend-icon="mdi-import"
@ -400,7 +428,13 @@ export default Vue.extend({
return this.$store.state.komgaSse.taskCountByType
},
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
return this.$store.getters.getLibraries
},
librariesPinned(): LibraryDto[] {
return this.$store.getters.getLibrariesPinned
},
librariesUnpinned(): LibraryDto[] {
return this.$store.getters.getLibrariesUnpinned
},
isAdmin(): boolean {
return this.$store.getters.meAdmin

View file

@ -108,7 +108,7 @@ export default Vue.extend({
},
computed: {
filterLibrariesOptions(): object[] {
return this.$store.state.komgaLibraries.libraries.map(x => ({
return this.$store.getters.getLibraries.map(x => ({
text: x.name,
value: x.id,
}))

View file

@ -0,0 +1,35 @@
<template>
<div class="pa-6">
<empty-state
:title="$t('no_libraries_pinned.title')"
:sub-title="$t('no_libraries_pinned.subtitle')"
icon="mdi-pin-off"
/>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import EmptyState from '@/components/EmptyState.vue'
export default Vue.extend({
name: 'NoPinnedLibraries',
components: {EmptyState},
mounted() {
if (this.$store.getters.getLibrariesPinned.length !== 0) {
this.$router.push({name: 'dashboard'})
}
},
watch: {
'$store.getters.getLibrariesPinned': {
handler(val) {
if (val.length !== 0) this.$router.push({name: 'dashboard'})
},
},
},
})
</script>
<style scoped>
</style>

View file

@ -43,7 +43,6 @@
import Vue from 'vue'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import {ERROR, ErrorEvent, NOTIFICATION, NotificationEvent} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import jsFileDownloader from 'js-file-downloader'
import urls from '@/functions/urls'
@ -53,11 +52,6 @@ export default Vue.extend({
data: () => ({
modalStopServer: false,
}),
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
},
methods: {
async cancelAllTasks() {
const count = await this.$komgaTasks.deleteAllTasks()