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 @@ + + + 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, +}