diff --git a/next-ui/src/colada/readlists.ts b/next-ui/src/colada/readlists.ts
new file mode 100644
index 000000000..8d3df9692
--- /dev/null
+++ b/next-ui/src/colada/readlists.ts
@@ -0,0 +1,16 @@
+import { defineMutation, useMutation } from '@pinia/colada'
+import { komgaClient } from '@/api/komga-client'
+import type { components } from '@/generated/openapi/komga'
+
+export const QUERY_KEYS_READLIST = {
+ root: ['readlists'] as const,
+}
+
+export const useCreateReadList = defineMutation(() => {
+ return useMutation({
+ mutation: (readList: components['schemas']['ReadListCreationDto']) =>
+ komgaClient.POST('/api/v1/readlists', {
+ body: readList,
+ }),
+ })
+})
diff --git a/next-ui/src/components.d.ts b/next-ui/src/components.d.ts
index b43034fba..7a9593166 100644
--- a/next-ui/src/components.d.ts
+++ b/next-ui/src/components.d.ts
@@ -33,6 +33,7 @@ declare module 'vue' {
FragmentHistoryExpandTable: typeof import('./fragments/fragment/history/expand/Table.vue')['default']
FragmentHistoryTable: typeof import('./fragments/fragment/history/Table.vue')['default']
FragmentImportBooksTransientBooksTable: typeof import('./fragments/fragment/import/books/TransientBooksTable.vue')['default']
+ FragmentImportReadlistTable: typeof import('./fragments/fragment/import/readlist/Table.vue')['default']
FragmentLocaleSelector: typeof import('./fragments/fragment/LocaleSelector.vue')['default']
FragmentRemoteFileList: typeof import('./fragments/fragment/RemoteFileList.vue')['default']
FragmentSnackQueue: typeof import('./fragments/fragment/SnackQueue.vue')['default']
diff --git a/next-ui/src/fragments/fragment/import/readlist/Table.stories.ts b/next-ui/src/fragments/fragment/import/readlist/Table.stories.ts
new file mode 100644
index 000000000..31791773a
--- /dev/null
+++ b/next-ui/src/fragments/fragment/import/readlist/Table.stories.ts
@@ -0,0 +1,97 @@
+import type { Meta, StoryObj } from '@storybook/vue3-vite'
+
+import { http, delay } from 'msw'
+import Table from './Table.vue'
+import { response400BadRequest } from '@/mocks/api/handlers'
+import SnackQueue from '@/fragments/fragment/SnackQueue.vue'
+import { matchCbl } from '@/mocks/api/handlers/readlists'
+import { expect, waitFor } from 'storybook/test'
+
+const meta = {
+ component: Table,
+ subcomponents: { SnackQueue },
+ render: (args: object) => ({
+ components: { Table, SnackQueue },
+ 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: {
+ match: matchCbl,
+ },
+}
+
+const singleMatch = {
+ ...matchCbl,
+ requests: [
+ {
+ request: { series: ['Space Adventures (2018)', 'Space Adventures'], number: '1' },
+ matches: [
+ {
+ series: {
+ seriesId: '63',
+ title: 'Space Adventures',
+ releaseDate: '2018-07-10',
+ },
+ books: [{ bookId: '0F99E5W723ENS', number: '1', title: 'Volume 1' }],
+ },
+ ],
+ },
+ ],
+}
+export const Created: Story = {
+ args: {
+ match: singleMatch,
+ },
+ play: async ({ canvas, userEvent }) => {
+ await userEvent.click(canvas.getByRole('button', { name: /create/i }))
+
+ await waitFor(() => expect(canvas.getByRole('button', { name: /create/i })).toBeDisabled())
+ },
+ render: (args: object) => ({
+ components: { Table, SnackQueue },
+ setup() {
+ return { args }
+ },
+ template: '',
+ }),
+}
+
+export const Empty: Story = {
+ args: {
+ match: { ...matchCbl, requests: [] },
+ },
+}
+
+export const Loading: Story = {
+ args: {
+ match: singleMatch,
+ },
+ parameters: {
+ msw: {
+ handlers: [http.all('*', async () => await delay(2_000))],
+ },
+ },
+}
+
+export const ErrorOnCreation: Story = {
+ args: {
+ match: singleMatch,
+ },
+ parameters: {
+ msw: {
+ handlers: [http.post('*/v1/readlists', response400BadRequest)],
+ },
+ },
+}
diff --git a/next-ui/src/fragments/fragment/import/readlist/Table.vue b/next-ui/src/fragments/fragment/import/readlist/Table.vue
new file mode 100644
index 000000000..2df2c524f
--- /dev/null
+++ b/next-ui/src/fragments/fragment/import/readlist/Table.vue
@@ -0,0 +1,506 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ s }}
+
+
+
+
+
+
+ {{ value?.title }}
+
+ {{ $formatDate(value?.releaseDate, { year: 'numeric', timeZone: 'UTC' }) }}
+
+
+
+
+
+
+
+
+
{{ value.number }} - {{ value.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ seriesPicked(series)"
+ />
+
+ bookPicked(book)"
+ />
+
+
+
+
+
diff --git a/next-ui/src/mocks/api/handlers.ts b/next-ui/src/mocks/api/handlers.ts
index 91a549b49..495eb8c49 100644
--- a/next-ui/src/mocks/api/handlers.ts
+++ b/next-ui/src/mocks/api/handlers.ts
@@ -12,6 +12,7 @@ import { seriesHandlers } from '@/mocks/api/handlers/series'
import { booksHandlers } from '@/mocks/api/handlers/books'
import { filesystemHandlers } from '@/mocks/api/handlers/filesystem'
import { transientBooksHandlers } from '@/mocks/api/handlers/transient-books'
+import { readListsHandlers } from '@/mocks/api/handlers/readlists'
export const handlers = [
...actuatorHandlers,
@@ -21,6 +22,7 @@ export const handlers = [
...filesystemHandlers,
...historyHandlers,
...librariesHandlers,
+ ...readListsHandlers,
...referentialHandlers,
...releasesHandlers,
...seriesHandlers,
diff --git a/next-ui/src/mocks/api/handlers/readlists.ts b/next-ui/src/mocks/api/handlers/readlists.ts
new file mode 100644
index 000000000..4c681c58b
--- /dev/null
+++ b/next-ui/src/mocks/api/handlers/readlists.ts
@@ -0,0 +1,151 @@
+import { httpTyped } from '@/mocks/api/httpTyped'
+
+export const matchCbl = {
+ readListMatch: { name: "Jupiter's Legacy", errorCode: '' },
+ requests: [
+ {
+ request: { series: ['Space Adventures (2018)', 'Space Adventures'], number: '1' },
+ matches: [
+ {
+ series: {
+ seriesId: '63',
+ title: 'Space Adventures',
+ releaseDate: '2018-07-10',
+ },
+ books: [{ bookId: '0F99E5W723ENS', number: '1', title: 'Volume 1' }],
+ },
+ ],
+ },
+ {
+ request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '2' },
+ matches: [
+ {
+ series: {
+ seriesId: '63',
+ title: 'Space Adventures',
+ releaseDate: '2018-07-10',
+ },
+ books: [{ bookId: '0F99E5W723ENS', number: '1', title: 'Volume 1' }],
+ },
+ ],
+ },
+ {
+ request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '3' },
+ matches: [
+ {
+ series: {
+ seriesId: '63',
+ title: "Jupiter's Legacy",
+ releaseDate: '2018-07-10',
+ },
+ books: [{ bookId: '0F99E5W763ECC', number: '3', title: 'Volume 3' }],
+ },
+ ],
+ },
+ {
+ request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '4' },
+ matches: [
+ {
+ series: {
+ seriesId: '63',
+ title: "Jupiter's Legacy",
+ releaseDate: '2018-07-10',
+ },
+ books: [{ bookId: '0F99E5W723ENT', number: '4', title: 'Volume 4' }],
+ },
+ ],
+ },
+ {
+ request: { series: ["Jupiter's Legacy (2013)", "Jupiter's Legacy"], number: '5' },
+ matches: [
+ {
+ series: {
+ seriesId: '63',
+ title: "Jupiter's Legacy",
+ releaseDate: '2018-07-10',
+ },
+ books: [{ bookId: '0F99E5W723ENR', number: '5', title: 'Volume 5' }],
+ },
+ ],
+ },
+ {
+ request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '1' },
+ matches: [],
+ },
+ {
+ request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '2' },
+ matches: [],
+ },
+ {
+ request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '3' },
+ matches: [],
+ },
+ {
+ request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '4' },
+ matches: [],
+ },
+ {
+ request: { series: ["Jupiter's Legacy 2 (2016)", "Jupiter's Legacy 2"], number: '5' },
+ matches: [],
+ },
+ {
+ request: {
+ series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"],
+ number: '1',
+ },
+ matches: [],
+ },
+ {
+ request: {
+ series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"],
+ number: '2',
+ },
+ matches: [],
+ },
+ {
+ request: {
+ series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"],
+ number: '3',
+ },
+ matches: [],
+ },
+ {
+ request: {
+ series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"],
+ number: '4',
+ },
+ matches: [],
+ },
+ {
+ request: {
+ series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"],
+ number: '5',
+ },
+ matches: [],
+ },
+ {
+ request: {
+ series: ["Jupiter's Legacy Requiem (2021)", "Jupiter's Legacy Requiem"],
+ number: '6',
+ },
+ matches: [],
+ },
+ ],
+ errorCode: '',
+}
+
+export const readListsHandlers = [
+ httpTyped.post('/api/v1/readlists', async ({ request, response }) => {
+ const body = await request.json()
+ return response(200).json({
+ ...body,
+ createdDate: new Date(),
+ lastModifiedDate: new Date(),
+ id: (Math.random() + 1).toString(36).substring(7),
+ filtered: false,
+ })
+ }),
+ httpTyped.post('/api/v1/readlists/match/comicrack', ({ response }) => {
+ return response(200).json(matchCbl)
+ }),
+]
diff --git a/next-ui/src/stores/messages.ts b/next-ui/src/stores/messages.ts
index 69ca869f5..e1b2d8597 100644
--- a/next-ui/src/stores/messages.ts
+++ b/next-ui/src/stores/messages.ts
@@ -1,8 +1,17 @@
// Utilities
import { defineStore } from 'pinia'
+type Message =
+ | {
+ text: string
+ color?: string
+ timer?: string | boolean
+ timeout?: string | number
+ }
+ | string
+
export const useMessagesStore = defineStore('messages', {
state: () => ({
- messages: [] as object[],
+ messages: [] as Message[],
}),
})