mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
scaffolding for library browsing
This commit is contained in:
parent
6a73408c3c
commit
e46afaa98d
25 changed files with 728 additions and 11 deletions
57
next-ui/src/colada/collections.ts
Normal file
57
next-ui/src/colada/collections.ts
Normal file
|
|
@ -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),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
@ -8,19 +8,21 @@ export const QUERY_KEYS_READLIST = {
|
||||||
bySearch: (request: object) => [...QUERY_KEYS_READLIST.root, JSON.stringify(request)] as const,
|
bySearch: (request: object) => [...QUERY_KEYS_READLIST.root, JSON.stringify(request)] as const,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useListReadLists = defineQueryOptions(
|
export const readListsListQuery = defineQueryOptions(
|
||||||
({
|
({
|
||||||
search,
|
search,
|
||||||
libraryId,
|
libraryIds,
|
||||||
|
pause = false,
|
||||||
pageRequest,
|
pageRequest,
|
||||||
}: {
|
}: {
|
||||||
search?: string
|
search?: string
|
||||||
libraryId?: string
|
libraryIds?: string[]
|
||||||
|
pause?: boolean
|
||||||
pageRequest?: PageRequest
|
pageRequest?: PageRequest
|
||||||
}) => ({
|
}) => ({
|
||||||
key: QUERY_KEYS_READLIST.bySearch({
|
key: QUERY_KEYS_READLIST.bySearch({
|
||||||
search: search,
|
search: search,
|
||||||
libraryId: libraryId,
|
libraryIds: libraryIds,
|
||||||
pageRequest: pageRequest,
|
pageRequest: pageRequest,
|
||||||
}),
|
}),
|
||||||
query: () =>
|
query: () =>
|
||||||
|
|
@ -29,13 +31,15 @@ export const useListReadLists = defineQueryOptions(
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
search: search,
|
search: search,
|
||||||
libraryId: libraryId,
|
library_id: libraryIds,
|
||||||
...pageRequest,
|
...pageRequest,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// unwrap the openapi-fetch structure on success
|
// unwrap the openapi-fetch structure on success
|
||||||
.then((res) => res.data),
|
.then((res) => res.data),
|
||||||
|
enabled: !pause,
|
||||||
|
placeholderData: (previousData: any) => previousData, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
3
next-ui/src/components.d.ts
vendored
3
next-ui/src/components.d.ts
vendored
|
|
@ -54,14 +54,17 @@ declare module 'vue' {
|
||||||
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
|
LayoutAppDrawerMenuMedia: typeof import('./components/layout/app/drawer/menu/Media.vue')['default']
|
||||||
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
|
LayoutAppDrawerMenuServer: typeof import('./components/layout/app/drawer/menu/Server.vue')['default']
|
||||||
LayoutAppDrawerReorderLibraries: typeof import('./components/layout/app/drawer/ReorderLibraries.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']
|
LibraryDeletionWarning: typeof import('./components/library/DeletionWarning.vue')['default']
|
||||||
LibraryFormCreateEdit: typeof import('./components/library/form/CreateEdit.vue')['default']
|
LibraryFormCreateEdit: typeof import('./components/library/form/CreateEdit.vue')['default']
|
||||||
LibraryFormStepGeneral: typeof import('./components/library/form/StepGeneral.vue')['default']
|
LibraryFormStepGeneral: typeof import('./components/library/form/StepGeneral.vue')['default']
|
||||||
LibraryFormStepMetadata: typeof import('./components/library/form/StepMetadata.vue')['default']
|
LibraryFormStepMetadata: typeof import('./components/library/form/StepMetadata.vue')['default']
|
||||||
LibraryFormStepOptions: typeof import('./components/library/form/StepOptions.vue')['default']
|
LibraryFormStepOptions: typeof import('./components/library/form/StepOptions.vue')['default']
|
||||||
LibraryFormStepScanner: typeof import('./components/library/form/StepScanner.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']
|
LibraryMenuLibraries: typeof import('./components/library/MenuLibraries.vue')['default']
|
||||||
LibraryMenuLibrary: typeof import('./components/library/MenuLibrary.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']
|
LocaleSelector: typeof import('./components/LocaleSelector.vue')['default']
|
||||||
PageHashKnownTable: typeof import('./components/pageHash/KnownTable.vue')['default']
|
PageHashKnownTable: typeof import('./components/pageHash/KnownTable.vue')['default']
|
||||||
PageHashMatchTable: typeof import('./components/pageHash/MatchTable.vue')['default']
|
PageHashMatchTable: typeof import('./components/pageHash/MatchTable.vue')['default']
|
||||||
|
|
|
||||||
|
|
@ -236,7 +236,7 @@ import {
|
||||||
import { useDisplay } from 'vuetify'
|
import { useDisplay } from 'vuetify'
|
||||||
import { useQuery } from '@pinia/colada'
|
import { useQuery } from '@pinia/colada'
|
||||||
import { bookListQuery } from '@/colada/books'
|
import { bookListQuery } from '@/colada/books'
|
||||||
import { useCreateReadList, useListReadLists } from '@/colada/readlists'
|
import { useCreateReadList, readListsListQuery } from '@/colada/readlists'
|
||||||
import { useMessagesStore } from '@/stores/messages'
|
import { useMessagesStore } from '@/stores/messages'
|
||||||
import type { ErrorCause } from '@/api/komga-client'
|
import type { ErrorCause } from '@/api/komga-client'
|
||||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||||
|
|
@ -335,7 +335,7 @@ watchImmediate(
|
||||||
)
|
)
|
||||||
|
|
||||||
//region Duplicate read list name check
|
//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(() =>
|
const readListNameAlreadyExists = computed(() =>
|
||||||
allReadLists.value?.content?.some(
|
allReadLists.value?.content?.some(
|
||||||
(it) => it.name.localeCompare(readListName.value, undefined, { sensitivity: 'accent' }) == 0,
|
(it) => it.name.localeCompare(readListName.value, undefined, { sensitivity: 'accent' }) == 0,
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
prepend-icon="i-mdi:bookshelf"
|
prepend-icon="i-mdi:bookshelf"
|
||||||
|
to="/libraries/pinned"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
<v-icon-btn
|
<v-icon-btn
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
icon="i-mdi:plus"
|
icon="i-mdi:plus"
|
||||||
|
variant="text"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
$formatMessage({
|
$formatMessage({
|
||||||
description: 'Add library button: aria label',
|
description: 'Add library button: aria label',
|
||||||
|
|
@ -23,11 +25,12 @@
|
||||||
@mouseenter="
|
@mouseenter="
|
||||||
(event: Event) => (dialogConfirmEdit.activator = event.currentTarget as Element)
|
(event: Event) => (dialogConfirmEdit.activator = event.currentTarget as Element)
|
||||||
"
|
"
|
||||||
@click="createLibrary"
|
@click.prevent="createLibrary"
|
||||||
/>
|
/>
|
||||||
<v-icon-btn
|
<v-icon-btn
|
||||||
id="ID01KC5N8S3V35QV04SYETY01M9H"
|
id="ID01KC5N8S3V35QV04SYETY01M9H"
|
||||||
icon="i-mdi:dots-vertical"
|
icon="i-mdi:dots-vertical"
|
||||||
|
variant="text"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
$formatMessage({
|
$formatMessage({
|
||||||
description: 'Libraries menu button: aria label',
|
description: 'Libraries menu button: aria label',
|
||||||
|
|
@ -35,6 +38,7 @@
|
||||||
id: 'hJEc5M',
|
id: 'hJEc5M',
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@click.prevent
|
||||||
/>
|
/>
|
||||||
<LibraryMenuLibraries activator-id="#ID01KC5N8S3V35QV04SYETY01M9H" />
|
<LibraryMenuLibraries activator-id="#ID01KC5N8S3V35QV04SYETY01M9H" />
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -44,6 +48,7 @@
|
||||||
v-for="library in pinned"
|
v-for="library in pinned"
|
||||||
:key="library.id"
|
:key="library.id"
|
||||||
:title="library.name"
|
:title="library.name"
|
||||||
|
:to="`/libraries/${library.id}`"
|
||||||
prepend-icon="blank"
|
prepend-icon="blank"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
|
|
@ -51,6 +56,7 @@
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
:id="`ID01KC5NTP02S3CMF12ZS2R4HNWX${library.id}`"
|
:id="`ID01KC5NTP02S3CMF12ZS2R4HNWX${library.id}`"
|
||||||
icon="i-mdi:dots-vertical"
|
icon="i-mdi:dots-vertical"
|
||||||
|
variant="text"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
$formatMessage({
|
$formatMessage({
|
||||||
description: 'Library menu button: aria label',
|
description: 'Library menu button: aria label',
|
||||||
|
|
@ -58,6 +64,7 @@
|
||||||
id: '3gimvl',
|
id: '3gimvl',
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@click.prevent
|
||||||
/>
|
/>
|
||||||
<LibraryMenuLibrary
|
<LibraryMenuLibrary
|
||||||
:activator-id="`#ID01KC5NTP02S3CMF12ZS2R4HNWX${library.id}`"
|
:activator-id="`#ID01KC5NTP02S3CMF12ZS2R4HNWX${library.id}`"
|
||||||
|
|
@ -88,6 +95,7 @@
|
||||||
v-for="library in unpinned"
|
v-for="library in unpinned"
|
||||||
:key="library.id"
|
:key="library.id"
|
||||||
:title="library.name"
|
:title="library.name"
|
||||||
|
:to="`/libraries/${library.id}`"
|
||||||
prepend-icon="blank"
|
prepend-icon="blank"
|
||||||
>
|
>
|
||||||
<template #append>
|
<template #append>
|
||||||
|
|
@ -95,6 +103,7 @@
|
||||||
v-if="isAdmin"
|
v-if="isAdmin"
|
||||||
:id="`ID01KC5QH18T79WTFFJWJ6ES4SFE${library.id}`"
|
:id="`ID01KC5QH18T79WTFFJWJ6ES4SFE${library.id}`"
|
||||||
icon="i-mdi:dots-vertical"
|
icon="i-mdi:dots-vertical"
|
||||||
|
variant="text"
|
||||||
:aria-label="
|
:aria-label="
|
||||||
$formatMessage({
|
$formatMessage({
|
||||||
description: 'Library menu button: aria label',
|
description: 'Library menu button: aria label',
|
||||||
|
|
@ -102,6 +111,7 @@
|
||||||
id: '3gimvl',
|
id: '3gimvl',
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
|
@click.prevent
|
||||||
/>
|
/>
|
||||||
<LibraryMenuLibrary
|
<LibraryMenuLibrary
|
||||||
:activator-id="`#ID01KC5QH18T79WTFFJWJ6ES4SFE${library.id}`"
|
:activator-id="`#ID01KC5QH18T79WTFFJWJ6ES4SFE${library.id}`"
|
||||||
|
|
@ -127,6 +137,7 @@ import type { ErrorCause } from '@/api/komga-client'
|
||||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||||
|
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
const router = useRouter()
|
||||||
const display = useDisplay()
|
const display = useDisplay()
|
||||||
const { unpinned, pinned, refresh } = useLibraries()
|
const { unpinned, pinned, refresh } = useLibraries()
|
||||||
const { isAdmin } = useCurrentUser()
|
const { isAdmin } = useCurrentUser()
|
||||||
|
|
|
||||||
82
next-ui/src/components/library/BottomNavigation.stories.ts
Normal file
82
next-ui/src/components/library/BottomNavigation.stories.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||||
|
|
||||||
|
import BottomNavigation from './BottomNavigation.vue'
|
||||||
|
import { expect, waitFor } from 'storybook/test'
|
||||||
|
|
||||||
|
const meta = {
|
||||||
|
component: BottomNavigation,
|
||||||
|
render: (args: object) => ({
|
||||||
|
components: { BottomNavigation },
|
||||||
|
setup() {
|
||||||
|
return { args }
|
||||||
|
},
|
||||||
|
template: '<BottomNavigation v-bind="args"/>',
|
||||||
|
}),
|
||||||
|
parameters: {
|
||||||
|
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
},
|
||||||
|
args: {},
|
||||||
|
} satisfies Meta<typeof BottomNavigation>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
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())
|
||||||
|
},
|
||||||
|
}
|
||||||
25
next-ui/src/components/library/BottomNavigation.vue
Normal file
25
next-ui/src/components/library/BottomNavigation.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<v-bottom-navigation grow>
|
||||||
|
<v-btn
|
||||||
|
v-for="(route, i) in routes"
|
||||||
|
:key="i"
|
||||||
|
:to="route.to"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
v-if="route.icon"
|
||||||
|
:icon="route.icon"
|
||||||
|
/>
|
||||||
|
<span>{{ route.title }}</span>
|
||||||
|
</v-btn>
|
||||||
|
</v-bottom-navigation>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Route } from '@/types/route'
|
||||||
|
|
||||||
|
const { routes } = defineProps<{
|
||||||
|
routes: Route[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
66
next-ui/src/components/library/Holder.vue
Normal file
66
next-ui/src/components/library/Holder.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<LibraryBottomNavigation
|
||||||
|
v-if="display.xs.value"
|
||||||
|
:routes="routes"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LibraryTabNavigation
|
||||||
|
v-else
|
||||||
|
:routes="routes"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { LibraryId } from '@/types/libraries'
|
||||||
|
import { useGetLibrariesById } from '@/composables/libraries'
|
||||||
|
import { useQuery } from '@pinia/colada'
|
||||||
|
import { collectionsListQuery } from '@/colada/collections'
|
||||||
|
import { PageRequest } from '@/types/PageRequest'
|
||||||
|
import { readListsListQuery } from '@/colada/readlists'
|
||||||
|
import { useDisplay } from 'vuetify'
|
||||||
|
|
||||||
|
const { libraryId } = defineProps<{
|
||||||
|
libraryId: LibraryId
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const display = useDisplay()
|
||||||
|
|
||||||
|
const { libraries } = useGetLibrariesById(libraryId)
|
||||||
|
const { data: collections } = useQuery(collectionsListQuery, () => ({
|
||||||
|
libraryIds: libraries.value?.map((it) => it.id),
|
||||||
|
pageRequest: PageRequest.Zero(),
|
||||||
|
pause: libraries.value === undefined,
|
||||||
|
}))
|
||||||
|
const { data: readlists } = useQuery(readListsListQuery, () => ({
|
||||||
|
libraryIds: libraries.value?.map((it) => it.id),
|
||||||
|
pageRequest: PageRequest.Zero(),
|
||||||
|
pause: libraries.value === undefined,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const routesBase = [
|
||||||
|
{ title: 'Recommended', icon: 'i-mdi:star', to: `/libraries/${libraryId}/recommended` },
|
||||||
|
{ title: 'Series', icon: 'i-mdi:bookshelf', to: `/libraries/${libraryId}/series` },
|
||||||
|
{ title: 'Books', icon: 'i-mdi:book-multiple', to: `/libraries/${libraryId}/books` },
|
||||||
|
]
|
||||||
|
|
||||||
|
const routes = computed(() => {
|
||||||
|
const extra = []
|
||||||
|
if ((collections.value?.totalElements ?? 0) > 0)
|
||||||
|
extra.push({
|
||||||
|
title: 'Collections',
|
||||||
|
icon: 'i-mdi:layers-triple',
|
||||||
|
to: `/libraries/${libraryId}/collections`,
|
||||||
|
})
|
||||||
|
if ((readlists.value?.totalElements ?? 0) > 0)
|
||||||
|
extra.push({
|
||||||
|
title: 'Read Lists',
|
||||||
|
icon: 'i-mdi:bookmark-multiple',
|
||||||
|
to: `/libraries/${libraryId}/readlists`,
|
||||||
|
})
|
||||||
|
return [...routesBase, ...extra]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -4,11 +4,16 @@
|
||||||
location="end"
|
location="end"
|
||||||
>
|
>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item
|
<template
|
||||||
v-for="(action, i) in actions"
|
v-for="(action, i) in actions"
|
||||||
:key="i"
|
:key="i"
|
||||||
v-bind="action"
|
>
|
||||||
/>
|
<v-divider v-if="action.divider" />
|
||||||
|
<v-list-item
|
||||||
|
v-else
|
||||||
|
v-bind="action"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -38,6 +43,7 @@ const actions = [
|
||||||
}),
|
}),
|
||||||
onClick: () => (appStore.reorderLibraries = true),
|
onClick: () => (appStore.reorderLibraries = true),
|
||||||
},
|
},
|
||||||
|
{ divider: true },
|
||||||
{
|
{
|
||||||
title: intl.formatMessage({
|
title: intl.formatMessage({
|
||||||
description: 'Libraries menu: scan',
|
description: 'Libraries menu: scan',
|
||||||
|
|
|
||||||
82
next-ui/src/components/library/TabNavigation.stories.ts
Normal file
82
next-ui/src/components/library/TabNavigation.stories.ts
Normal file
|
|
@ -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: '<TabNavigation v-bind="args"/>',
|
||||||
|
}),
|
||||||
|
parameters: {
|
||||||
|
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||||
|
},
|
||||||
|
args: {},
|
||||||
|
} satisfies Meta<typeof TabNavigation>
|
||||||
|
|
||||||
|
export default meta
|
||||||
|
type Story = StoryObj<typeof meta>
|
||||||
|
|
||||||
|
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())
|
||||||
|
},
|
||||||
|
}
|
||||||
20
next-ui/src/components/library/TabNavigation.vue
Normal file
20
next-ui/src/components/library/TabNavigation.vue
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<v-tabs :items="routes">
|
||||||
|
<template #tab="{ item: route }">
|
||||||
|
<v-tab
|
||||||
|
:text="route.title"
|
||||||
|
:to="route.to"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</v-tabs>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Route } from '@/types/route'
|
||||||
|
|
||||||
|
const { routes } = defineProps<{
|
||||||
|
routes: Route[]
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
88
next-ui/src/composables/libraries.test.ts
Normal file
88
next-ui/src/composables/libraries.test.ts
Normal file
|
|
@ -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<string, ClientSettingUserLibrary> = {
|
||||||
|
'3': {
|
||||||
|
unpinned: true,
|
||||||
|
},
|
||||||
|
'4': {
|
||||||
|
unpinned: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const settings: Record<string, components['schemas']['ClientSettingUserUpdateDto']> = {
|
||||||
|
[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)
|
||||||
|
}
|
||||||
37
next-ui/src/composables/libraries.ts
Normal file
37
next-ui/src/composables/libraries.ts
Normal file
|
|
@ -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<LibraryId>) {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@ import { transientBooksHandlers } from '@/mocks/api/handlers/transient-books'
|
||||||
import { readListsHandlers } from '@/mocks/api/handlers/readlists'
|
import { readListsHandlers } from '@/mocks/api/handlers/readlists'
|
||||||
import { pageHashesHandlers } from '@/mocks/api/handlers/page-hashes'
|
import { pageHashesHandlers } from '@/mocks/api/handlers/page-hashes'
|
||||||
import { clientSettingsHandlers } from '@/mocks/api/handlers/client-settings'
|
import { clientSettingsHandlers } from '@/mocks/api/handlers/client-settings'
|
||||||
|
import { collectionsHandlers } from '@/mocks/api/handlers/collections'
|
||||||
|
|
||||||
export const handlers = [
|
export const handlers = [
|
||||||
...actuatorHandlers,
|
...actuatorHandlers,
|
||||||
|
|
@ -22,6 +23,7 @@ export const handlers = [
|
||||||
...booksHandlers,
|
...booksHandlers,
|
||||||
...claimHandlers,
|
...claimHandlers,
|
||||||
...clientSettingsHandlers,
|
...clientSettingsHandlers,
|
||||||
|
...collectionsHandlers,
|
||||||
...filesystemHandlers,
|
...filesystemHandlers,
|
||||||
...historyHandlers,
|
...historyHandlers,
|
||||||
...librariesHandlers,
|
...librariesHandlers,
|
||||||
|
|
|
||||||
47
next-ui/src/mocks/api/handlers/collections.ts
Normal file
47
next-ui/src/mocks/api/handlers/collections.ts
Normal file
|
|
@ -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',
|
||||||
|
// },
|
||||||
|
// })
|
||||||
|
// }),
|
||||||
|
]
|
||||||
22
next-ui/src/pages/libraries/[id].vue
Normal file
22
next-ui/src/pages/libraries/[id].vue
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<LibraryHolder
|
||||||
|
:key="libraryId"
|
||||||
|
:library-id="libraryId"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const libraryId = computed(() => route.params.id)
|
||||||
|
|
||||||
|
//TODO: for now we always redirect to 'recommended', this should be persisted per libraryId
|
||||||
|
watch(libraryId, () => {
|
||||||
|
void router.push({ name: '/libraries/[id]/recommended', params: { id: libraryId.value } })
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
requiresRole: ADMIN
|
||||||
|
</route>
|
||||||
12
next-ui/src/pages/libraries/[id]/books.vue
Normal file
12
next-ui/src/pages/libraries/[id]/books.vue
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
series.vue
|
||||||
|
<template>
|
||||||
|
BOOKS
|
||||||
|
<EmptyStateConstruction />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup></script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
requiresRole: ADMIN
|
||||||
|
</route>
|
||||||
13
next-ui/src/pages/libraries/[id]/collections.vue
Normal file
13
next-ui/src/pages/libraries/[id]/collections.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
COLLECTIONS
|
||||||
|
<EmptyStateConstruction />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
requiresRole: ADMIN
|
||||||
|
</route>
|
||||||
13
next-ui/src/pages/libraries/[id]/readlists.vue
Normal file
13
next-ui/src/pages/libraries/[id]/readlists.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
READLISTS
|
||||||
|
<EmptyStateConstruction />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
requiresRole: ADMIN
|
||||||
|
</route>
|
||||||
13
next-ui/src/pages/libraries/[id]/recommended.vue
Normal file
13
next-ui/src/pages/libraries/[id]/recommended.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
RECOMMENDED
|
||||||
|
<EmptyStateConstruction />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
requiresRole: ADMIN
|
||||||
|
</route>
|
||||||
13
next-ui/src/pages/libraries/[id]/series.vue
Normal file
13
next-ui/src/pages/libraries/[id]/series.vue
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
SERIES
|
||||||
|
<EmptyStateConstruction />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
//
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
meta:
|
||||||
|
requiresRole: ADMIN
|
||||||
|
</route>
|
||||||
87
next-ui/src/typed-router.d.ts
vendored
87
next-ui/src/typed-router.d.ts
vendored
|
|
@ -100,6 +100,52 @@ declare module 'vue-router/auto-routes' {
|
||||||
Record<never, never>,
|
Record<never, never>,
|
||||||
| never
|
| never
|
||||||
>,
|
>,
|
||||||
|
'/libraries/[id]': RouteRecordInfo<
|
||||||
|
'/libraries/[id]',
|
||||||
|
'/libraries/:id',
|
||||||
|
{ id: ParamValue<true> },
|
||||||
|
{ id: ParamValue<false> },
|
||||||
|
| '/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<true> },
|
||||||
|
{ id: ParamValue<false> },
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/libraries/[id]/collections': RouteRecordInfo<
|
||||||
|
'/libraries/[id]/collections',
|
||||||
|
'/libraries/:id/collections',
|
||||||
|
{ id: ParamValue<true> },
|
||||||
|
{ id: ParamValue<false> },
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/libraries/[id]/readlists': RouteRecordInfo<
|
||||||
|
'/libraries/[id]/readlists',
|
||||||
|
'/libraries/:id/readlists',
|
||||||
|
{ id: ParamValue<true> },
|
||||||
|
{ id: ParamValue<false> },
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/libraries/[id]/recommended': RouteRecordInfo<
|
||||||
|
'/libraries/[id]/recommended',
|
||||||
|
'/libraries/:id/recommended',
|
||||||
|
{ id: ParamValue<true> },
|
||||||
|
{ id: ParamValue<false> },
|
||||||
|
| never
|
||||||
|
>,
|
||||||
|
'/libraries/[id]/series': RouteRecordInfo<
|
||||||
|
'/libraries/[id]/series',
|
||||||
|
'/libraries/:id/series',
|
||||||
|
{ id: ParamValue<true> },
|
||||||
|
{ id: ParamValue<false> },
|
||||||
|
| never
|
||||||
|
>,
|
||||||
'/login': RouteRecordInfo<
|
'/login': RouteRecordInfo<
|
||||||
'/login',
|
'/login',
|
||||||
'/login',
|
'/login',
|
||||||
|
|
@ -277,6 +323,47 @@ declare module 'vue-router/auto-routes' {
|
||||||
views:
|
views:
|
||||||
| never
|
| 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': {
|
'src/pages/login.vue': {
|
||||||
routes:
|
routes:
|
||||||
| '/login'
|
| '/login'
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,10 @@ export class PageRequest {
|
||||||
return new PageRequest(undefined, undefined, undefined, true)
|
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
|
* Can be used from v-data-table-server @update:options
|
||||||
* @param page
|
* @param page
|
||||||
|
|
|
||||||
5
next-ui/src/types/libraries.ts
Normal file
5
next-ui/src/types/libraries.ts
Normal file
|
|
@ -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'
|
||||||
5
next-ui/src/types/route.ts
Normal file
5
next-ui/src/types/route.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type Route = {
|
||||||
|
title: string
|
||||||
|
icon?: string
|
||||||
|
to: string
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue