diff --git a/next-ui/src/api/images.ts b/next-ui/src/api/images.ts
index aa65641a..8a342dba 100644
--- a/next-ui/src/api/images.ts
+++ b/next-ui/src/api/images.ts
@@ -1,25 +1,30 @@
import { ApiBaseUrl } from '@/api/base'
-export function seriesThumbnailUrl(seriesId?: string): string | undefined {
+export function seriesPosterUrl(seriesId?: string): string | undefined {
if (seriesId) return `${ApiBaseUrl.noSlash}/api/v1/series/${seriesId}/thumbnail`
return undefined
}
-export function bookThumbnailUrl(bookId?: string): string | undefined {
+export function bookPosterUrl(bookId?: string): string | undefined {
if (bookId) return `${ApiBaseUrl.noSlash}/api/v1/books/${bookId}/thumbnail`
return undefined
}
+export function collectionPosterUrl(collectionId?: string): string | undefined {
+ if (collectionId) return `${ApiBaseUrl.noSlash}/api/v1/collections/${collectionId}/thumbnail`
+ return undefined
+}
+
+export function readListPosterUrl(readList?: string): string | undefined {
+ if (readList) return `${ApiBaseUrl.noSlash}/api/v1/readlists/${readList}/thumbnail`
+ return undefined
+}
+
export function bookPageThumbnailUrl(bookId?: string, page?: number): string | undefined {
if (bookId && page) return `${ApiBaseUrl.noSlash}/api/v1/books/${bookId}/pages/${page}/thumbnail`
return undefined
}
-export function collectionThumbnailUrl(collectionId?: string): string | undefined {
- if (collectionId) return `${ApiBaseUrl.noSlash}/api/v1/collections/${collectionId}/thumbnail`
- return undefined
-}
-
export function pageHashKnownThumbnailUrl(hash?: string): string | undefined {
if (hash) return `${ApiBaseUrl.noSlash}/api/v1/page-hashes/${hash}/thumbnail`
return undefined
diff --git a/next-ui/src/colada/readlists.ts b/next-ui/src/colada/readlists.ts
index df986217..4b08d7c0 100644
--- a/next-ui/src/colada/readlists.ts
+++ b/next-ui/src/colada/readlists.ts
@@ -48,3 +48,41 @@ export const useCreateReadList = defineMutation(() => {
}),
})
})
+
+export const useUpdateReadList = defineMutation(() => {
+ // const queryCache = useQueryCache()
+ return useMutation({
+ mutation: ({
+ readListId,
+ data,
+ }: {
+ readListId: string
+ data: components['schemas']['ReadListUpdateDto']
+ }) =>
+ komgaClient.PATCH('/api/v1/readlists/{id}', {
+ params: {
+ path: {
+ id: readListId,
+ },
+ },
+ body: data,
+ }),
+ onSuccess: () => {
+ //TODO: check how to invalidate cache
+ // void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root })
+ },
+ })
+})
+
+export const useDeleteReadList = defineMutation(() =>
+ useMutation({
+ mutation: (readListId: string) =>
+ komgaClient.DELETE('/api/v1/readlists/{id}', {
+ params: {
+ path: {
+ id: readListId,
+ },
+ },
+ }),
+ }),
+)
diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index a2ef1856..20b40c9d 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -110,6 +110,10 @@ declare module 'vue' {
PagingSelector: typeof import('./components/PagingSelector.vue')['default']
PosterSizeSlider: typeof import('./components/PosterSizeSlider.vue')['default']
PresentationSelector: typeof import('./components/PresentationSelector.vue')['default']
+ ReadlistCard: typeof import('./components/readlist/card/ReadlistCard.vue')['default']
+ ReadlistDeletionWarning: typeof import('./components/readlist/DeletionWarning.vue')['default']
+ ReadlistMenu: typeof import('./components/readlist/menu/ReadlistMenu.vue')['default']
+ ReadlistMenuBottomSheet: typeof import('./components/readlist/menu/ReadlistMenuBottomSheet.vue')['default']
ReleaseCard: typeof import('./components/release/Card.vue')['default']
RemoteFileList: typeof import('./components/RemoteFileList.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
diff --git a/next-ui/src/components/book/card/BookCard.vue b/next-ui/src/components/book/card/BookCard.vue
index b4dfdc49..fcaa0e51 100644
--- a/next-ui/src/components/book/card/BookCard.vue
+++ b/next-ui/src/components/book/card/BookCard.vue
@@ -3,7 +3,7 @@
:id="id"
:title="titleAndLines.title"
:lines="titleAndLines.lines"
- :poster-url="bookThumbnailUrl(book.id)"
+ :poster-url="bookPosterUrl(book.id)"
:top-right-icon="isRead ? 'i-mdi:check' : undefined"
:progress-percent="isRead ? undefined : progressPercent"
fab-icon="i-mdi:play"
@@ -28,7 +28,7 @@
diff --git a/next-ui/src/components/readlist/card/ReadlistCard.stories.ts b/next-ui/src/components/readlist/card/ReadlistCard.stories.ts
new file mode 100644
index 00000000..76120e4a
--- /dev/null
+++ b/next-ui/src/components/readlist/card/ReadlistCard.stories.ts
@@ -0,0 +1,66 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import ReadlistCard from './ReadlistCard.vue'
+import { fn } from 'storybook/test'
+import { httpTyped } from '@/mocks/api/httpTyped'
+import { userRegular } from '@/mocks/api/handlers/users'
+import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
+import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
+import { mockReadList1 } from '@/mocks/api/handlers/readlists'
+
+const meta = {
+ component: ReadlistCard,
+ render: (args: object) => ({
+ components: { ReadlistCard, DialogConfirmEditInstance, DialogConfirmInstance },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ docs: {
+ description: {
+ component: '',
+ },
+ },
+ },
+ args: {
+ readList: mockReadList1,
+ onSelection: fn(),
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {},
+}
+
+export const Selected: Story = {
+ args: {
+ selected: true,
+ },
+}
+
+export const Hover: Story = {
+ args: {},
+ play: ({ canvas, userEvent }) => {
+ userEvent.hover(canvas.getByRole('img'))
+ },
+}
+
+export const HoverNonAdmin: Story = {
+ args: {},
+ parameters: {
+ msw: {
+ handlers: [
+ httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
+ ],
+ },
+ },
+ play: ({ canvas, userEvent }) => {
+ userEvent.hover(canvas.getByRole('img'))
+ },
+}
diff --git a/next-ui/src/components/readlist/card/ReadlistCard.vue b/next-ui/src/components/readlist/card/ReadlistCard.vue
new file mode 100644
index 00000000..c7453de1
--- /dev/null
+++ b/next-ui/src/components/readlist/card/ReadlistCard.vue
@@ -0,0 +1,90 @@
+
+ emit('selection', val, event)"
+ @click-quick-action="showEditMetadataDialog()"
+ @card-long-press="bottomSheet = true"
+ />
+
+
+
+
+
diff --git a/next-ui/src/components/readlist/menu/ReadlistMenu.mdx b/next-ui/src/components/readlist/menu/ReadlistMenu.mdx
new file mode 100644
index 00000000..b3173cd0
--- /dev/null
+++ b/next-ui/src/components/readlist/menu/ReadlistMenu.mdx
@@ -0,0 +1,9 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+
+import * as Stories from './ReadlistMenu.stories.ts';
+
+
+
+# ReadlistMenu
+
+Action menu for read list.
diff --git a/next-ui/src/components/readlist/menu/ReadlistMenu.stories.ts b/next-ui/src/components/readlist/menu/ReadlistMenu.stories.ts
new file mode 100644
index 00000000..7a09aa85
--- /dev/null
+++ b/next-ui/src/components/readlist/menu/ReadlistMenu.stories.ts
@@ -0,0 +1,54 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import ReadlistMenu from './ReadlistMenu.vue'
+import { expect } from 'storybook/test'
+import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
+import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
+import { mockReadList1 } from '@/mocks/api/handlers/readlists'
+
+const meta = {
+ component: ReadlistMenu,
+ render: (args: object) => ({
+ components: { ReadlistMenu, DialogConfirmInstance, DialogConfirmEditInstance },
+ setup() {
+ return { args }
+ },
+ template:
+ '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ docs: {
+ description: {
+ component: '',
+ },
+ },
+ },
+ args: {
+ activator: '#IDce0b073e6b2146e688c1cd32b61f3fef',
+ readList: mockReadList1,
+ },
+ play: async ({ canvas, userEvent }) => {
+ await expect(canvas.getByRole('button')).toBeEnabled()
+
+ await userEvent.click(canvas.getByRole('button'))
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {},
+}
+
+// export const NonAdmin: Story = {
+// args: {},
+// parameters: {
+// msw: {
+// handlers: [
+// httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
+// ],
+// },
+// },
+// }
diff --git a/next-ui/src/components/readlist/menu/ReadlistMenu.vue b/next-ui/src/components/readlist/menu/ReadlistMenu.vue
new file mode 100644
index 00000000..d188822d
--- /dev/null
+++ b/next-ui/src/components/readlist/menu/ReadlistMenu.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.mdx b/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.mdx
new file mode 100644
index 00000000..f3ecd7c2
--- /dev/null
+++ b/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.mdx
@@ -0,0 +1,9 @@
+import { Meta } from '@storybook/addon-docs/blocks';
+
+import * as Stories from './ReadlistMenuBottomSheet.stories.ts';
+
+
+
+# ReadlistMenuBottomSheet
+
+Action menu for read list for touch screens.
diff --git a/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.stories.ts b/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.stories.ts
new file mode 100644
index 00000000..5dba9be0
--- /dev/null
+++ b/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.stories.ts
@@ -0,0 +1,48 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import ReadlistMenuBottomSheet from './ReadlistMenuBottomSheet.vue'
+import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
+import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
+import { mockReadList1 } from '@/mocks/api/handlers/readlists'
+
+const meta = {
+ component: ReadlistMenuBottomSheet,
+ render: (args: object) => ({
+ components: { ReadlistMenuBottomSheet, DialogConfirmInstance, DialogConfirmEditInstance },
+ setup() {
+ return { args }
+ },
+ template:
+ '',
+ }),
+ parameters: {
+ // More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
+ docs: {
+ description: {
+ component: '',
+ },
+ },
+ },
+ args: {
+ modelValue: true,
+ readList: mockReadList1,
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ args: {},
+}
+
+// export const NonAdmin: Story = {
+// args: {},
+// parameters: {
+// msw: {
+// handlers: [
+// httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
+// ],
+// },
+// },
+// }
diff --git a/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.vue b/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.vue
new file mode 100644
index 00000000..0bc369b5
--- /dev/null
+++ b/next-ui/src/components/readlist/menu/ReadlistMenuBottomSheet.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/next-ui/src/components/series/CardWide/SeriesCardWide.vue b/next-ui/src/components/series/CardWide/SeriesCardWide.vue
index e7569dda..920004e2 100644
--- a/next-ui/src/components/series/CardWide/SeriesCardWide.vue
+++ b/next-ui/src/components/series/CardWide/SeriesCardWide.vue
@@ -2,7 +2,7 @@
import type { components } from '@/generated/openapi/komga'
-import { seriesThumbnailUrl } from '@/api/images'
+import { seriesPosterUrl } from '@/api/images'
import type { ItemCardEmits, ItemCardProps } from '@/types/ItemCard'
import { useCurrentUser } from '@/colada/users'
diff --git a/next-ui/src/components/series/card/SeriesCard.vue b/next-ui/src/components/series/card/SeriesCard.vue
index ce1f5839..1302c32f 100644
--- a/next-ui/src/components/series/card/SeriesCard.vue
+++ b/next-ui/src/components/series/card/SeriesCard.vue
@@ -3,7 +3,7 @@
:id="id"
:title="title"
:lines="lines"
- :poster-url="seriesThumbnailUrl(series.id)"
+ :poster-url="seriesPosterUrl(series.id)"
:top-right="unreadCount"
:top-right-icon="isRead ? 'i-mdi:check' : undefined"
fab-icon="i-mdi:play"
@@ -28,7 +28,7 @@
+
+
meta:
requiresRole: USER
diff --git a/next-ui/src/types/readlist.ts b/next-ui/src/types/readlist.ts
new file mode 100644
index 00000000..a19874fd
--- /dev/null
+++ b/next-ui/src/types/readlist.ts
@@ -0,0 +1,4 @@
+export enum ReadListAction {
+ EDIT,
+ DELETE,
+}