diff --git a/next-ui/src/colada/collections.ts b/next-ui/src/colada/collections.ts
new file mode 100644
index 00000000..75ae64d7
--- /dev/null
+++ b/next-ui/src/colada/collections.ts
@@ -0,0 +1,57 @@
+import { defineQueryOptions } from '@pinia/colada'
+import { komgaClient } from '@/api/komga-client'
+import type { PageRequest } from '@/types/PageRequest'
+
+export const QUERY_KEYS_COLLECTIONS = {
+ root: ['collections'] as const,
+ bySearch: (request: object) => [...QUERY_KEYS_COLLECTIONS.root, JSON.stringify(request)] as const,
+ byId: (seriesId: string) => [...QUERY_KEYS_COLLECTIONS.root, seriesId] as const,
+}
+
+export const collectionsListQuery = defineQueryOptions(
+ ({
+ search,
+ libraryIds,
+ pause = false,
+ pageRequest,
+ }: {
+ search?: string
+ libraryIds?: string[]
+ pause?: boolean
+ pageRequest?: PageRequest
+ }) => ({
+ key: QUERY_KEYS_COLLECTIONS.bySearch({ search: search, libraryIds, pageRequest: pageRequest }),
+ query: () =>
+ komgaClient
+ .GET('/api/v1/collections', {
+ params: {
+ query: {
+ search: search,
+ library_id: libraryIds,
+ ...pageRequest,
+ },
+ },
+ })
+ // unwrap the openapi-fetch structure on success
+ .then((res) => res.data),
+ enabled: !pause,
+ placeholderData: (previousData: any) => previousData, // eslint-disable-line @typescript-eslint/no-explicit-any
+ }),
+)
+
+export const collectionDetailQuery = defineQueryOptions(
+ ({ collectionId }: { collectionId: string }) => ({
+ key: QUERY_KEYS_COLLECTIONS.byId(collectionId),
+ query: () =>
+ komgaClient
+ .GET('/api/v1/collections/{id}', {
+ params: {
+ path: {
+ id: collectionId,
+ },
+ },
+ })
+ // unwrap the openapi-fetch structure on success
+ .then((res) => res.data),
+ }),
+)
diff --git a/next-ui/src/colada/readlists.ts b/next-ui/src/colada/readlists.ts
index 44414814..26390b99 100644
--- a/next-ui/src/colada/readlists.ts
+++ b/next-ui/src/colada/readlists.ts
@@ -8,19 +8,21 @@ export const QUERY_KEYS_READLIST = {
bySearch: (request: object) => [...QUERY_KEYS_READLIST.root, JSON.stringify(request)] as const,
}
-export const useListReadLists = defineQueryOptions(
+export const readListsListQuery = defineQueryOptions(
({
search,
- libraryId,
+ libraryIds,
+ pause = false,
pageRequest,
}: {
search?: string
- libraryId?: string
+ libraryIds?: string[]
+ pause?: boolean
pageRequest?: PageRequest
}) => ({
key: QUERY_KEYS_READLIST.bySearch({
search: search,
- libraryId: libraryId,
+ libraryIds: libraryIds,
pageRequest: pageRequest,
}),
query: () =>
@@ -29,13 +31,15 @@ export const useListReadLists = defineQueryOptions(
params: {
query: {
search: search,
- libraryId: libraryId,
+ library_id: libraryIds,
...pageRequest,
},
},
})
// unwrap the openapi-fetch structure on success
.then((res) => res.data),
+ enabled: !pause,
+ placeholderData: (previousData: any) => previousData, // eslint-disable-line @typescript-eslint/no-explicit-any
}),
)
diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index 6638d4fb..a15c622d 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -54,14 +54,17 @@ declare module 'vue' {
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
LayoutAppDrawerReorderLibraries: typeof import('./components/layout/app/drawer/ReorderLibraries.vue')['default']
+ LibraryBottomNavigation: typeof import('./components/library/BottomNavigation.vue')['default']
LibraryDeletionWarning: typeof import('./components/library/DeletionWarning.vue')['default']
LibraryFormCreateEdit: typeof import('./components/library/form/CreateEdit.vue')['default']
LibraryFormStepGeneral: typeof import('./components/library/form/StepGeneral.vue')['default']
LibraryFormStepMetadata: typeof import('./components/library/form/StepMetadata.vue')['default']
LibraryFormStepOptions: typeof import('./components/library/form/StepOptions.vue')['default']
LibraryFormStepScanner: typeof import('./components/library/form/StepScanner.vue')['default']
+ LibraryHolder: typeof import('./components/library/Holder.vue')['default']
LibraryMenuLibraries: typeof import('./components/library/MenuLibraries.vue')['default']
LibraryMenuLibrary: typeof import('./components/library/MenuLibrary.vue')['default']
+ LibraryTabNavigation: typeof import('./components/library/TabNavigation.vue')['default']
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
PageHashKnownTable: typeof import('./components/pageHash/KnownTable.vue')['default']
PageHashMatchTable: typeof import('./components/pageHash/MatchTable.vue')['default']
diff --git a/next-ui/src/components/import/readlist/Table.vue b/next-ui/src/components/import/readlist/Table.vue
index f4c73edc..710618e3 100644
--- a/next-ui/src/components/import/readlist/Table.vue
+++ b/next-ui/src/components/import/readlist/Table.vue
@@ -236,7 +236,7 @@ import {
import { useDisplay } from 'vuetify'
import { useQuery } from '@pinia/colada'
import { bookListQuery } from '@/colada/books'
-import { useCreateReadList, useListReadLists } from '@/colada/readlists'
+import { useCreateReadList, readListsListQuery } from '@/colada/readlists'
import { useMessagesStore } from '@/stores/messages'
import type { ErrorCause } from '@/api/komga-client'
import { commonMessages } from '@/utils/i18n/common-messages'
@@ -335,7 +335,7 @@ watchImmediate(
)
//region Duplicate read list name check
-const { data: allReadLists } = useQuery(useListReadLists({ pageRequest: PageRequest.Unpaged() }))
+const { data: allReadLists } = useQuery(readListsListQuery({ pageRequest: PageRequest.Unpaged() }))
const readListNameAlreadyExists = computed(() =>
allReadLists.value?.content?.some(
(it) => it.name.localeCompare(readListName.value, undefined, { sensitivity: 'accent' }) == 0,
diff --git a/next-ui/src/components/layout/app/drawer/menu/Libraries.vue b/next-ui/src/components/layout/app/drawer/menu/Libraries.vue
index feb7183c..a71f5535 100644
--- a/next-ui/src/components/layout/app/drawer/menu/Libraries.vue
+++ b/next-ui/src/components/layout/app/drawer/menu/Libraries.vue
@@ -8,11 +8,13 @@
})
"
prepend-icon="i-mdi:bookshelf"
+ to="/libraries/pinned"
>
(dialogConfirmEdit.activator = event.currentTarget as Element)
"
- @click="createLibrary"
+ @click.prevent="createLibrary"
/>
@@ -44,6 +48,7 @@
v-for="library in pinned"
:key="library.id"
:title="library.name"
+ :to="`/libraries/${library.id}`"
prepend-icon="blank"
>
@@ -51,6 +56,7 @@
v-if="isAdmin"
:id="`ID01KC5NTP02S3CMF12ZS2R4HNWX${library.id}`"
icon="i-mdi:dots-vertical"
+ variant="text"
:aria-label="
$formatMessage({
description: 'Library menu button: aria label',
@@ -58,6 +64,7 @@
id: '3gimvl',
})
"
+ @click.prevent
/>
@@ -95,6 +103,7 @@
v-if="isAdmin"
:id="`ID01KC5QH18T79WTFFJWJ6ES4SFE${library.id}`"
icon="i-mdi:dots-vertical"
+ variant="text"
:aria-label="
$formatMessage({
description: 'Library menu button: aria label',
@@ -102,6 +111,7 @@
id: '3gimvl',
})
"
+ @click.prevent
/>
({
+ components: { BottomNavigation },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ },
+ args: {},
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ { title: 'Collections', icon: 'i-mdi:layers-triple', to: '' },
+ { title: 'Read Lists', icon: 'i-mdi:bookmark-multiple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).not.toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).not.toBeNull())
+ },
+}
+
+export const NoCollection: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ { title: 'Read Lists', icon: 'i-mdi:bookmark-multiple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).not.toBeNull())
+ },
+}
+
+export const NoReadList: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ { title: 'Collections', icon: 'i-mdi:layers-triple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).not.toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).toBeNull())
+ },
+}
+
+export const NoCollectionNorReadList: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).toBeNull())
+ },
+}
diff --git a/next-ui/src/components/library/BottomNavigation.vue b/next-ui/src/components/library/BottomNavigation.vue
new file mode 100644
index 00000000..999ddca4
--- /dev/null
+++ b/next-ui/src/components/library/BottomNavigation.vue
@@ -0,0 +1,25 @@
+
+
+
+
+ {{ route.title }}
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/library/Holder.vue b/next-ui/src/components/library/Holder.vue
new file mode 100644
index 00000000..eb0f5fae
--- /dev/null
+++ b/next-ui/src/components/library/Holder.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/library/MenuLibraries.vue b/next-ui/src/components/library/MenuLibraries.vue
index 1c3752c1..bd065a69 100644
--- a/next-ui/src/components/library/MenuLibraries.vue
+++ b/next-ui/src/components/library/MenuLibraries.vue
@@ -4,11 +4,16 @@
location="end"
>
-
+ >
+
+
+
@@ -38,6 +43,7 @@ const actions = [
}),
onClick: () => (appStore.reorderLibraries = true),
},
+ { divider: true },
{
title: intl.formatMessage({
description: 'Libraries menu: scan',
diff --git a/next-ui/src/components/library/TabNavigation.stories.ts b/next-ui/src/components/library/TabNavigation.stories.ts
new file mode 100644
index 00000000..4bac6425
--- /dev/null
+++ b/next-ui/src/components/library/TabNavigation.stories.ts
@@ -0,0 +1,82 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import TabNavigation from './TabNavigation.vue'
+import { expect, waitFor } from 'storybook/test'
+
+const meta = {
+ component: TabNavigation,
+ render: (args: object) => ({
+ components: { TabNavigation },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ },
+ args: {},
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ { title: 'Collections', icon: 'i-mdi:layers-triple', to: '' },
+ { title: 'Read Lists', icon: 'i-mdi:bookmark-multiple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).not.toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).not.toBeNull())
+ },
+}
+
+export const NoCollection: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ { title: 'Read Lists', icon: 'i-mdi:bookmark-multiple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).not.toBeNull())
+ },
+}
+
+export const NoReadList: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ { title: 'Collections', icon: 'i-mdi:layers-triple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).not.toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).toBeNull())
+ },
+}
+
+export const NoCollectionNorReadList: Story = {
+ args: {
+ routes: [
+ { title: 'Recommended', icon: 'i-mdi:star', to: '' },
+ { title: 'Series', icon: 'i-mdi:bookshelf', to: '' },
+ { title: 'Books', icon: 'i-mdi:book-multiple', to: '' },
+ ],
+ },
+ play: async ({ canvas }) => {
+ await waitFor(() => expect(canvas.queryByText(/collections/i)).toBeNull())
+ await waitFor(() => expect(canvas.queryByText(/read lists/i)).toBeNull())
+ },
+}
diff --git a/next-ui/src/components/library/TabNavigation.vue b/next-ui/src/components/library/TabNavigation.vue
new file mode 100644
index 00000000..753ed40f
--- /dev/null
+++ b/next-ui/src/components/library/TabNavigation.vue
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/composables/libraries.test.ts b/next-ui/src/composables/libraries.test.ts
new file mode 100644
index 00000000..871a56f7
--- /dev/null
+++ b/next-ui/src/composables/libraries.test.ts
@@ -0,0 +1,88 @@
+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from 'vitest'
+import { server } from '@/mocks/api/node'
+import { createMockColada } from '@/mocks/pinia-colada'
+import { enableAutoUnmount } from '@vue/test-utils'
+import { useGetLibrariesById } from '@/composables/libraries'
+import { httpTyped } from '@/mocks/api/httpTyped'
+import { libraries } from '@/mocks/api/handlers/libraries'
+import type { components } from '@/generated/openapi/komga'
+import { CLIENT_SETTING_USER, type ClientSettingUserLibrary } from '@/types/ClientSettingsUser'
+import { waitFor } from 'storybook/test'
+import type { LibraryId } from '@/types/libraries'
+
+beforeAll(() => server.listen())
+beforeEach(() =>
+ server.use(
+ httpTyped.get('/api/v1/libraries', ({ response }) => {
+ const bds = {
+ ...libraries[0],
+ id: '3',
+ name: 'BDs',
+ } as components['schemas']['LibraryDto']
+ const magazines = {
+ ...libraries[0],
+ id: '4',
+ name: 'Magazines',
+ } as components['schemas']['LibraryDto']
+ const manga = {
+ ...libraries[0],
+ id: '5',
+ name: 'Mangas',
+ } as components['schemas']['LibraryDto']
+ const libs = [bds, magazines, manga]
+ return response(200).json(libs)
+ }),
+ httpTyped.get('/api/v1/client-settings/user/list', ({ response }) => {
+ const userLibraries: Record = {
+ '3': {
+ unpinned: true,
+ },
+ '4': {
+ unpinned: true,
+ },
+ }
+ const settings: Record = {
+ [CLIENT_SETTING_USER.NEXTUI_LIBRARIES]: {
+ value: JSON.stringify(userLibraries),
+ },
+ }
+ return response(200).json(settings)
+ }),
+ ),
+)
+afterEach(() => server.resetHandlers())
+afterAll(() => server.close())
+
+enableAutoUnmount(afterEach)
+
+describe('libraries composable', () => {
+ test("when getting 'all' libraries then values are correct", async () => {
+ await doTest('all', ['3', '4', '5'])
+ })
+
+ test("when getting 'pinned' libraries then values are correct", async () => {
+ await doTest('pinned', ['5'])
+ })
+
+ test("when getting 'unpinned' libraries then values are correct", async () => {
+ await doTest('unpinned', ['3', '4'])
+ })
+
+ test('when getting specific ID then values are correct', async () => {
+ await doTest('4', ['4'])
+ })
+
+ test('when getting non-existent ID then values are correct', async () => {
+ await doTest('ABC', [])
+ })
+})
+
+async function doTest(libraryId: LibraryId, expectedIds: string[]) {
+ createMockColada(() => useGetLibrariesById(libraryId))
+ const { libraries } = useGetLibrariesById(libraryId)
+
+ await waitFor(() => {
+ if (libraries.value === undefined) throw new Error('data not fetched')
+ })
+ expect(libraries.value?.map((it) => it.id)).toStrictEqual(expectedIds)
+}
diff --git a/next-ui/src/composables/libraries.ts b/next-ui/src/composables/libraries.ts
new file mode 100644
index 00000000..40eb699b
--- /dev/null
+++ b/next-ui/src/composables/libraries.ts
@@ -0,0 +1,37 @@
+import { useLibraries } from '@/colada/libraries'
+import type { LibraryId } from '@/types/libraries'
+import type { components } from '@/generated/openapi/komga'
+
+/**
+ * A composable that returns libraries filtered by a LibraryId.
+ * @param libraryId the library ID or group to get
+ */
+export function useGetLibrariesById(libraryId: MaybeRefOrGetter) {
+ const { data: all, pinned, unpinned, status } = useLibraries()
+
+ const libs = computed(() => {
+ if (status.value !== 'success') return undefined
+
+ let libs: components['schemas']['LibraryDto'][] = []
+ switch (toValue(libraryId)) {
+ case 'all':
+ libs = all.value || []
+ break
+ case 'pinned':
+ libs = pinned.value
+ break
+ case 'unpinned':
+ libs = unpinned.value
+ break
+ default:
+ const lib = all.value?.find((it) => it.id === libraryId)
+ if (lib) libs = [lib]
+ break
+ }
+ return libs
+ })
+
+ return {
+ libraries: libs,
+ }
+}
diff --git a/next-ui/src/mocks/api/handlers.ts b/next-ui/src/mocks/api/handlers.ts
index f42b8c0c..744e81f5 100644
--- a/next-ui/src/mocks/api/handlers.ts
+++ b/next-ui/src/mocks/api/handlers.ts
@@ -15,6 +15,7 @@ import { transientBooksHandlers } from '@/mocks/api/handlers/transient-books'
import { readListsHandlers } from '@/mocks/api/handlers/readlists'
import { pageHashesHandlers } from '@/mocks/api/handlers/page-hashes'
import { clientSettingsHandlers } from '@/mocks/api/handlers/client-settings'
+import { collectionsHandlers } from '@/mocks/api/handlers/collections'
export const handlers = [
...actuatorHandlers,
@@ -22,6 +23,7 @@ export const handlers = [
...booksHandlers,
...claimHandlers,
...clientSettingsHandlers,
+ ...collectionsHandlers,
...filesystemHandlers,
...historyHandlers,
...librariesHandlers,
diff --git a/next-ui/src/mocks/api/handlers/collections.ts b/next-ui/src/mocks/api/handlers/collections.ts
new file mode 100644
index 00000000..6a445b9c
--- /dev/null
+++ b/next-ui/src/mocks/api/handlers/collections.ts
@@ -0,0 +1,47 @@
+import { httpTyped } from '@/mocks/api/httpTyped'
+import { PageRequest } from '@/types/PageRequest'
+import { mockPage } from '@/mocks/api/pageable'
+
+const collection1 = {
+ id: '026801S4HWRZA',
+ name: 'Golden Age',
+ ordered: true,
+ seriesIds: ['57'],
+ createdDate: new Date('2020-08-06T06:13:25Z'),
+ lastModifiedDate: new Date('2020-08-06T06:17:12Z'),
+ filtered: false,
+}
+
+const collections = [collection1]
+
+export const collectionsHandlers = [
+ httpTyped.get('/api/v1/collections', async ({ query, response }) => {
+ const search = query.get('search')
+
+ const selected = collections.filter((it) => {
+ let include = true
+ if (search) include = include && !!it.name.match(new RegExp(search, 'i'))
+ return include
+ })
+
+ return response(200).json(
+ mockPage(selected, new PageRequest(Number(query.get('page')), Number(query.get('size')))),
+ )
+ }),
+ // httpTyped.get('/api/v1/series/{seriesId}', ({ params, response }) => {
+ // if (params.seriesId === '404') return response(404).empty()
+ // return response(200).json(
+ // Object.assign({}, series1, { metadata: { title: `Series ${params.seriesId}` } }),
+ // )
+ // }),
+ // http.get('*/api/v1/series/*/thumbnail', async () => {
+ // // Get an ArrayBuffer from reading the file from disk or fetching it.
+ // const buffer = await fetch(mockThumbnailUrl).then((response) => response.arrayBuffer())
+ //
+ // return HttpResponse.arrayBuffer(buffer, {
+ // headers: {
+ // 'content-type': 'image/jpg',
+ // },
+ // })
+ // }),
+]
diff --git a/next-ui/src/pages/libraries/[id].vue b/next-ui/src/pages/libraries/[id].vue
new file mode 100644
index 00000000..899cda69
--- /dev/null
+++ b/next-ui/src/pages/libraries/[id].vue
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+meta:
+ requiresRole: ADMIN
+
diff --git a/next-ui/src/pages/libraries/[id]/books.vue b/next-ui/src/pages/libraries/[id]/books.vue
new file mode 100644
index 00000000..99155563
--- /dev/null
+++ b/next-ui/src/pages/libraries/[id]/books.vue
@@ -0,0 +1,12 @@
+series.vue
+
+ BOOKS
+
+
+
+
+
+
+meta:
+ requiresRole: ADMIN
+
diff --git a/next-ui/src/pages/libraries/[id]/collections.vue b/next-ui/src/pages/libraries/[id]/collections.vue
new file mode 100644
index 00000000..8f400b68
--- /dev/null
+++ b/next-ui/src/pages/libraries/[id]/collections.vue
@@ -0,0 +1,13 @@
+
+ COLLECTIONS
+
+
+
+
+
+
+meta:
+ requiresRole: ADMIN
+
diff --git a/next-ui/src/pages/libraries/[id]/readlists.vue b/next-ui/src/pages/libraries/[id]/readlists.vue
new file mode 100644
index 00000000..9a8e48f3
--- /dev/null
+++ b/next-ui/src/pages/libraries/[id]/readlists.vue
@@ -0,0 +1,13 @@
+
+ READLISTS
+
+
+
+
+
+
+meta:
+ requiresRole: ADMIN
+
diff --git a/next-ui/src/pages/libraries/[id]/recommended.vue b/next-ui/src/pages/libraries/[id]/recommended.vue
new file mode 100644
index 00000000..21c27160
--- /dev/null
+++ b/next-ui/src/pages/libraries/[id]/recommended.vue
@@ -0,0 +1,13 @@
+
+ RECOMMENDED
+
+
+
+
+
+
+meta:
+ requiresRole: ADMIN
+
diff --git a/next-ui/src/pages/libraries/[id]/series.vue b/next-ui/src/pages/libraries/[id]/series.vue
new file mode 100644
index 00000000..0d547ff5
--- /dev/null
+++ b/next-ui/src/pages/libraries/[id]/series.vue
@@ -0,0 +1,13 @@
+
+ SERIES
+
+
+
+
+
+
+meta:
+ requiresRole: ADMIN
+
diff --git a/next-ui/src/typed-router.d.ts b/next-ui/src/typed-router.d.ts
index ec28f9bf..39673ce6 100644
--- a/next-ui/src/typed-router.d.ts
+++ b/next-ui/src/typed-router.d.ts
@@ -100,6 +100,52 @@ declare module 'vue-router/auto-routes' {
Record,
| never
>,
+ '/libraries/[id]': RouteRecordInfo<
+ '/libraries/[id]',
+ '/libraries/:id',
+ { id: ParamValue },
+ { id: ParamValue },
+ | '/libraries/[id]/books'
+ | '/libraries/[id]/collections'
+ | '/libraries/[id]/readlists'
+ | '/libraries/[id]/recommended'
+ | '/libraries/[id]/series'
+ >,
+ '/libraries/[id]/books': RouteRecordInfo<
+ '/libraries/[id]/books',
+ '/libraries/:id/books',
+ { id: ParamValue },
+ { id: ParamValue },
+ | never
+ >,
+ '/libraries/[id]/collections': RouteRecordInfo<
+ '/libraries/[id]/collections',
+ '/libraries/:id/collections',
+ { id: ParamValue },
+ { id: ParamValue },
+ | never
+ >,
+ '/libraries/[id]/readlists': RouteRecordInfo<
+ '/libraries/[id]/readlists',
+ '/libraries/:id/readlists',
+ { id: ParamValue },
+ { id: ParamValue },
+ | never
+ >,
+ '/libraries/[id]/recommended': RouteRecordInfo<
+ '/libraries/[id]/recommended',
+ '/libraries/:id/recommended',
+ { id: ParamValue },
+ { id: ParamValue },
+ | never
+ >,
+ '/libraries/[id]/series': RouteRecordInfo<
+ '/libraries/[id]/series',
+ '/libraries/:id/series',
+ { id: ParamValue },
+ { id: ParamValue },
+ | never
+ >,
'/login': RouteRecordInfo<
'/login',
'/login',
@@ -277,6 +323,47 @@ declare module 'vue-router/auto-routes' {
views:
| never
}
+ 'src/pages/libraries/[id].vue': {
+ routes:
+ | '/libraries/[id]'
+ | '/libraries/[id]/books'
+ | '/libraries/[id]/collections'
+ | '/libraries/[id]/readlists'
+ | '/libraries/[id]/recommended'
+ | '/libraries/[id]/series'
+ views:
+ | 'default'
+ }
+ 'src/pages/libraries/[id]/books.vue': {
+ routes:
+ | '/libraries/[id]/books'
+ views:
+ | never
+ }
+ 'src/pages/libraries/[id]/collections.vue': {
+ routes:
+ | '/libraries/[id]/collections'
+ views:
+ | never
+ }
+ 'src/pages/libraries/[id]/readlists.vue': {
+ routes:
+ | '/libraries/[id]/readlists'
+ views:
+ | never
+ }
+ 'src/pages/libraries/[id]/recommended.vue': {
+ routes:
+ | '/libraries/[id]/recommended'
+ views:
+ | never
+ }
+ 'src/pages/libraries/[id]/series.vue': {
+ routes:
+ | '/libraries/[id]/series'
+ views:
+ | never
+ }
'src/pages/login.vue': {
routes:
| '/login'
diff --git a/next-ui/src/types/PageRequest.ts b/next-ui/src/types/PageRequest.ts
index 7e1f4c02..ce12d321 100644
--- a/next-ui/src/types/PageRequest.ts
+++ b/next-ui/src/types/PageRequest.ts
@@ -17,6 +17,10 @@ export class PageRequest {
return new PageRequest(undefined, undefined, undefined, true)
}
+ static Zero(): PageRequest {
+ return new PageRequest(undefined, 0, undefined, undefined)
+ }
+
/**
* Can be used from v-data-table-server @update:options
* @param page
diff --git a/next-ui/src/types/libraries.ts b/next-ui/src/types/libraries.ts
new file mode 100644
index 00000000..5456f806
--- /dev/null
+++ b/next-ui/src/types/libraries.ts
@@ -0,0 +1,5 @@
+/**
+ * Represents either a specific library ID, or a group of libraries
+ */
+// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
+export type LibraryId = string | 'all' | 'pinned' | 'unpinned'
diff --git a/next-ui/src/types/route.ts b/next-ui/src/types/route.ts
new file mode 100644
index 00000000..bc3f2f35
--- /dev/null
+++ b/next-ui/src/types/route.ts
@@ -0,0 +1,5 @@
+export type Route = {
+ title: string
+ icon?: string
+ to: string
+}