Skip to content

Commit c8e4a46

Browse files
committed
feat(webui): add ability to pin/unpin libraries
Closes: #1560
1 parent 4892945 commit c8e4a46

25 files changed

+311
-82
lines changed

komga-webui/src/components/LibraryNavigation.vue

+11-2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
import Vue from 'vue'
8181
import {COLLECTION_ADDED, COLLECTION_DELETED, READLIST_ADDED, READLIST_DELETED} from '@/types/events'
8282
import {LIBRARIES_ALL} from '@/types/library'
83+
import {LibraryDto} from '@/types/komga-libraries'
8384
8485
export default Vue.extend({
8586
name: 'LibraryNavigation',
@@ -107,6 +108,14 @@ export default Vue.extend({
107108
},
108109
immediate: true,
109110
},
111+
'$store.getters.getLibrariesPinned': {
112+
handler(val) {
113+
if (this.libraryId === LIBRARIES_ALL) {
114+
this.loadCollectionCounts(this.libraryId)
115+
this.loadReadListCounts(this.libraryId)
116+
}
117+
},
118+
},
110119
},
111120
created() {
112121
this.$eventHub.$on(COLLECTION_ADDED, this.collectionAdded)
@@ -142,12 +151,12 @@ export default Vue.extend({
142151
if(this.collectionsCount === 1) this.loadCollectionCounts(this.libraryId)
143152
},
144153
async loadCollectionCounts(libraryId: string) {
145-
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
154+
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
146155
this.$komgaCollections.getCollections(lib, {size: 0})
147156
.then(v => this.collectionsCount = v.totalElements)
148157
},
149158
async loadReadListCounts(libraryId: string) {
150-
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
159+
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
151160
await this.$komgaReadLists.getReadLists(lib, {size: 0})
152161
.then(v => this.readListsCount = v.totalElements)
153162
},

komga-webui/src/components/dialogs/CollectionEditDialog.vue

-4
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
import {UserRoles} from '@/types/enum-users'
101101
import Vue from 'vue'
102102
import {ERROR, ErrorEvent} from '@/types/events'
103-
import {LibraryDto} from '@/types/komga-libraries'
104103
import ThumbnailCard from '@/components/ThumbnailCard.vue'
105104
import DropZone from '@/components/DropZone.vue'
106105
@@ -152,9 +151,6 @@ export default Vue.extend({
152151
},
153152
},
154153
computed: {
155-
libraries(): LibraryDto[] {
156-
return this.$store.state.komgaLibraries.libraries
157-
},
158154
getErrorsName(): string {
159155
if (this.form.name === '') return this.$t('common.required').toString()
160156
if (this.form.name?.toLowerCase() !== this.collection.name?.toLowerCase() && this.collections.some(e => e.name.toLowerCase() === this.form.name.toLowerCase())) {

komga-webui/src/components/dialogs/ReadListEditDialog.vue

-4
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,6 @@
113113
import {UserRoles} from '@/types/enum-users'
114114
import Vue from 'vue'
115115
import {ERROR, ErrorEvent} from '@/types/events'
116-
import {LibraryDto} from '@/types/komga-libraries'
117116
import DropZone from '@/components/DropZone.vue'
118117
import ThumbnailCard from '@/components/ThumbnailCard.vue'
119118
import {ReadListDto, ReadListThumbnailDto, ReadListUpdateDto} from '@/types/komga-readlists'
@@ -167,9 +166,6 @@ export default Vue.extend({
167166
},
168167
},
169168
computed: {
170-
libraries(): LibraryDto[] {
171-
return this.$store.state.komgaLibraries.libraries
172-
},
173169
getErrorsName(): string {
174170
if (this.form.name === '') return this.$t('common.required').toString()
175171
if (this.form.name?.toLowerCase() !== this.readList.name?.toLowerCase() && this.readLists.some(e => e.name.toLowerCase() === this.form.name.toLowerCase())) {

komga-webui/src/components/dialogs/UserEditDialog.vue

-4
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@
4343
import {UserRoles} from '@/types/enum-users'
4444
import Vue from 'vue'
4545
import {ERROR} from '@/types/events'
46-
import {LibraryDto} from '@/types/komga-libraries'
4746
import {UserDto, UserUpdateDto} from '@/types/komga-users'
4847
4948
export default Vue.extend({
@@ -79,9 +78,6 @@ export default Vue.extend({
7978
value: x,
8079
}))
8180
},
82-
libraries(): LibraryDto[] {
83-
return this.$store.state.komgaLibraries.libraries
84-
},
8581
},
8682
methods: {
8783
dialogReset(user: UserDto) {

komga-webui/src/components/dialogs/UserRestrictionsEditDialog.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ export default Vue.extend({
188188
},
189189
computed: {
190190
libraries(): LibraryDto[] {
191-
return this.$store.state.komgaLibraries.libraries
191+
return this.$store.getters.getLibraries
192192
},
193193
ageRestrictionsAvailable(): any[] {
194194
return [

komga-webui/src/components/menus/LibraryActionsMenu.vue

+33-8
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,40 @@
11
<template>
22
<div>
3-
<v-menu offset-y v-if="isAdmin">
3+
<v-menu offset-y>
44
<template v-slot:activator="{ on }">
55
<v-btn icon v-on="on" @click.prevent="">
66
<v-icon>mdi-dots-vertical</v-icon>
77
</v-btn>
88
</template>
99
<v-list dense>
10-
<v-list-item @click="scan(false)">
10+
<v-list-item v-if="!library.unpinned" @click="unpin">
11+
<v-list-item-title>{{ $t('menu.unpin') }}</v-list-item-title>
12+
</v-list-item>
13+
<v-list-item v-if="library.unpinned" @click="pin">
14+
<v-list-item-title>{{ $t('menu.pin') }}</v-list-item-title>
15+
</v-list-item>
16+
<v-list-item @click="scan(false)" v-if="isAdmin">
1117
<v-list-item-title>{{ $t('menu.scan_library_files') }}</v-list-item-title>
1218
</v-list-item>
13-
<v-list-item @click="scan(true)" class="list-warning">
19+
<v-list-item @click="scan(true)" class="list-warning" v-if="isAdmin">
1420
<v-list-item-title>{{ $t('menu.scan_library_files_deep') }}</v-list-item-title>
1521
</v-list-item>
16-
<v-list-item @click="confirmAnalyzeModal = true">
22+
<v-list-item @click="confirmAnalyzeModal = true" v-if="isAdmin">
1723
<v-list-item-title>{{ $t('menu.analyze') }}</v-list-item-title>
1824
</v-list-item>
19-
<v-list-item @click="confirmRefreshMetadataModal = true">
25+
<v-list-item @click="confirmRefreshMetadataModal = true" v-if="isAdmin">
2026
<v-list-item-title>{{ $t('menu.refresh_metadata') }}</v-list-item-title>
2127
</v-list-item>
22-
<v-list-item @click="confirmEmptyTrash = true">
28+
<v-list-item @click="confirmEmptyTrash = true" v-if="isAdmin">
2329
<v-list-item-title>{{ $t('menu.empty_trash') }}</v-list-item-title>
2430
</v-list-item>
25-
<v-list-item @click="edit">
31+
<v-list-item @click="edit" v-if="isAdmin">
2632
<v-list-item-title>{{ $t('menu.edit') }}</v-list-item-title>
2733
</v-list-item>
2834
<v-list-item @click="promptDeleteLibrary"
29-
class="list-danger">
35+
class="list-danger"
36+
v-if="isAdmin"
37+
>
3038
<v-list-item-title>{{ $t('menu.delete') }}</v-list-item-title>
3139
</v-list-item>
3240
</v-list>
@@ -61,6 +69,7 @@
6169
import Vue from 'vue'
6270
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
6371
import {LibraryDto} from '@/types/komga-libraries'
72+
import {ClientSettingLibraryUpdate} from '@/types/komga-clientsettings'
6473
6574
export default Vue.extend({
6675
name: 'LibraryActionsMenu',
@@ -84,6 +93,22 @@ export default Vue.extend({
8493
},
8594
},
8695
methods: {
96+
unpin() {
97+
this.$store.dispatch('updateLibrarySetting', {
98+
libraryId: this.library.id,
99+
patch: {
100+
unpinned: true,
101+
},
102+
} as ClientSettingLibraryUpdate)
103+
},
104+
pin() {
105+
this.$store.dispatch('updateLibrarySetting', {
106+
libraryId: this.library.id,
107+
patch: {
108+
unpinned: false,
109+
},
110+
} as ClientSettingLibraryUpdate)
111+
},
87112
scan(scanDeep: boolean) {
88113
this.$komgaLibraries.scanLibrary(this.library, scanDeep)
89114
},

komga-webui/src/locales/en.json

+9-1
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@
248248
"locale_rtl": "false",
249249
"lock_all": "Lock all",
250250
"media": "Media",
251+
"more": "More",
251252
"n_selected": "{count} selected",
252253
"nothing_to_show": "Nothing to show",
253254
"ok": "OK",
@@ -261,6 +262,7 @@
261262
"password": "Password",
262263
"pdf": "PDF",
263264
"pending_tasks": "No pending tasks | 1 pending task | {count} pending tasks",
265+
"pinned_libraries": "Pinned Libraries",
264266
"publisher": "Publisher",
265267
"read": "Read",
266268
"read_on": "Read on {date}",
@@ -892,10 +894,12 @@
892894
"empty_trash": "Empty trash",
893895
"mark_read": "Mark as read",
894896
"mark_unread": "Mark as unread",
897+
"pin": "Pin",
895898
"refresh_metadata": "Refresh metadata",
896899
"scan_library_files": "Scan library files",
897900
"scan_library_files_deep": "Scan library files (deep)",
898-
"select_all": "Select all"
901+
"select_all": "Select all",
902+
"unpin": "Unpin"
899903
},
900904
"metrics": {
901905
"library_books": "Books per library",
@@ -914,6 +918,10 @@
914918
"libraries": "Libraries",
915919
"logout": "Log Out"
916920
},
921+
"no_libraries_pinned": {
922+
"title": "No pinned libraries",
923+
"subtitle": "You can pin a library from the 3-dots menu"
924+
},
917925
"page_not_found": {
918926
"go_back_to_home_page": "Go back to home page",
919927
"page_does_not_exist": "The page you are looking for doesn't exist.",

komga-webui/src/main.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Vue.use(Chartkick.use(Chart))
5959

6060
Vue.use(httpPlugin)
6161
Vue.use(logger)
62+
Vue.use(komgaSettings, {store: store, http: Vue.prototype.$http})
6263
Vue.use(komgaFileSystem, {http: Vue.prototype.$http})
6364
Vue.use(komgaSeries, {http: Vue.prototype.$http})
6465
Vue.use(komgaCollections, {http: Vue.prototype.$http})
@@ -80,7 +81,6 @@ Vue.use(komgaMetrics, {http: Vue.prototype.$http})
8081
Vue.use(komgaHistory, {http: Vue.prototype.$http})
8182
Vue.use(komgaAnnouncements, {http: Vue.prototype.$http})
8283
Vue.use(komgaReleases, {http: Vue.prototype.$http})
83-
Vue.use(komgaSettings, {store: store, http: Vue.prototype.$http})
8484
Vue.use(komgaFonts, {http: Vue.prototype.$http})
8585

8686
Vue.config.productionTip = false

komga-webui/src/plugins/komga-libraries.plugin.ts

+21-9
Original file line numberDiff line numberDiff line change
@@ -11,35 +11,47 @@ const vuexModule: Module<any, any> = {
1111
libraries: [] as LibraryDto[],
1212
},
1313
getters: {
14-
getLibraryById: (state) => (id: number) => {
15-
return state.libraries.find((l: any) => l.id === id)
14+
getLibraries(state, getters) {
15+
const settings = getters.getClientSettingsLibraries
16+
return state.libraries
17+
.map((it: LibraryDto) => Object.assign({}, it, settings[it.id]))
18+
.sort((a: LibraryDto, b: LibraryDto) => a.name.toLowerCase() > b.name.toLowerCase())
19+
},
20+
getLibraryById: (state, getters) => (id: number) => {
21+
return getters.getLibraries.find((l: any) => l.id === id)
22+
},
23+
getLibrariesPinned(state, getters) {
24+
return getters.getLibraries.filter((it: LibraryDto) => !it.unpinned)
25+
},
26+
getLibrariesUnpinned(state, getters) {
27+
return getters.getLibraries.filter((it: LibraryDto) => it.unpinned)
1628
},
1729
},
1830
mutations: {
19-
setLibraries (state, libraries) {
31+
setLibraries(state, libraries) {
2032
state.libraries = libraries
2133
},
2234
},
2335
actions: {
24-
async getLibraries ({ commit }) {
36+
async getLibraries({commit}) {
2537
commit('setLibraries', await service.getLibraries())
2638
},
27-
async postLibrary ({ dispatch }, library) {
39+
async postLibrary({dispatch}, library) {
2840
await service.postLibrary(library)
2941
},
30-
async updateLibrary ({ dispatch }, { libraryId, library }) {
42+
async updateLibrary({dispatch}, {libraryId, library}) {
3143
await service.updateLibrary(libraryId, library)
3244
},
33-
async deleteLibrary ({ dispatch }, library) {
45+
async deleteLibrary({dispatch}, library) {
3446
await service.deleteLibrary(library)
3547
},
3648
},
3749
}
3850

3951
export default {
40-
install (
52+
install(
4153
Vue: typeof _Vue,
42-
{ store, http }: { store: any, http: AxiosInstance }) {
54+
{store, http}: { store: any, http: AxiosInstance }) {
4355
service = new KomgaLibrariesService(http)
4456
Vue.prototype.$komgaLibraries = service
4557

komga-webui/src/plugins/komga-settings.plugin.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import {AxiosInstance} from 'axios'
22
import _Vue from 'vue'
33
import KomgaSettingsService from '@/services/komga-settings.service'
44
import {Module} from 'vuex'
5-
import {ClientSettingDto} from '@/types/komga-clientsettings'
5+
import {
6+
CLIENT_SETTING,
7+
ClientSettingDto,
8+
ClientSettingLibrary,
9+
ClientSettingLibraryUpdate,
10+
ClientSettingUserUpdateDto,
11+
} from '@/types/komga-clientsettings'
612

713
let service: KomgaSettingsService
814

@@ -15,6 +21,14 @@ const vuexModule: Module<any, any> = {
1521
getClientSettings(state): Record<string, ClientSettingDto> {
1622
return {...state.clientSettingsGlobal, ...state.clientSettingsUser}
1723
},
24+
getClientSettingsLibraries(state): Record<string, ClientSettingLibrary> {
25+
let settings: Record<string, ClientSettingLibrary> = {}
26+
try {
27+
settings = JSON.parse(state.clientSettingsUser[CLIENT_SETTING.WEBUI_LIBRARIES]?.value)
28+
} catch (e) {
29+
}
30+
return settings
31+
},
1832
},
1933
mutations: {
2034
setClientSettingsGlobal(state, settings) {
@@ -31,6 +45,16 @@ const vuexModule: Module<any, any> = {
3145
async getClientSettingsUser({commit}) {
3246
commit('setClientSettingsUser', await service.getClientSettingsUser())
3347
},
48+
async updateLibrarySetting({dispatch, getters}, update: ClientSettingLibraryUpdate) {
49+
const all = getters.getClientSettingsLibraries
50+
all[update.libraryId] = Object.assign({}, all[update.libraryId], update.patch)
51+
const newSettings = {} as Record<string, ClientSettingUserUpdateDto>
52+
newSettings[CLIENT_SETTING.WEBUI_LIBRARIES] = {
53+
value: JSON.stringify(all),
54+
}
55+
await service.updateClientSettingUser(newSettings)
56+
dispatch('getClientSettingsUser')
57+
},
3458
},
3559
}
3660

komga-webui/src/router.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ const noLibraryGuard = (to: any, from: any, next: any) => {
2121
} else next()
2222
}
2323

24+
const noLibraryNorPinGuard = (to: any, from: any, next: any) => {
25+
if (lStore.state.komgaLibraries.libraries.length === 0) {
26+
next({name: 'welcome'})
27+
} else if (lStore.getters.getLibrariesPinned.length === 0) {
28+
next({name: 'no-pins'})
29+
} else next()
30+
}
31+
2432
const getLibraryRoute = (libraryId: string) => {
2533
switch ((lStore.getters.getLibraryRoute(libraryId) as LIBRARY_ROUTE)) {
2634
case LIBRARY_ROUTE.COLLECTIONS:
@@ -57,10 +65,15 @@ const router = new Router({
5765
name: 'welcome',
5866
component: () => import(/* webpackChunkName: "welcome" */ './views/WelcomeView.vue'),
5967
},
68+
{
69+
path: '/no-pins',
70+
name: 'no-pins',
71+
component: () => import(/* webpackChunkName: "no-pins" */ './views/NoPinnedLibraries.vue'),
72+
},
6073
{
6174
path: '/dashboard',
6275
name: 'dashboard',
63-
beforeEnter: noLibraryGuard,
76+
beforeEnter: noLibraryNorPinGuard,
6477
component: () => import(/* webpackChunkName: "dashboard" */ './views/DashboardView.vue'),
6578
},
6679
{

komga-webui/src/services/komga-books.service.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,15 @@ export default class KomgaBooksService {
5353
}
5454
}
5555

56-
async getBooksOnDeck(libraryId?: string, pageRequest?: PageRequest): Promise<Page<BookDto>> {
56+
async getBooksOnDeck(libraryIds?: string[], pageRequest?: PageRequest): Promise<Page<BookDto>> {
5757
try {
5858
const params = {...pageRequest} as any
59-
if (libraryId) {
60-
params.library_id = libraryId
59+
if (libraryIds) {
60+
params.library_id = libraryIds
6161
}
6262
return (await this.http.get(`${API_BOOKS}/ondeck`, {
6363
params: params,
64+
paramsSerializer: params => qs.stringify(params, {indices: false}),
6465
})).data
6566
} catch (e) {
6667
let msg = 'An error occurred while trying to retrieve books on deck'

0 commit comments

Comments
 (0)