mirror of
https://github.com/gotson/komga.git
synced 2025-12-06 08:32:25 +01:00
add history page
This commit is contained in:
parent
2f7b4b1137
commit
13972ab4a1
29 changed files with 1140 additions and 7 deletions
|
|
@ -64,7 +64,7 @@ export default defineConfigWithVueTs(
|
|||
'error',
|
||||
{
|
||||
idInterpolationPattern: '[sha512:contenthash:base64:6]',
|
||||
idWhitelist: ['app.*'],
|
||||
idWhitelist: ['app.*', 'enum.*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
|||
4
next-ui/src/api/images.ts
Normal file
4
next-ui/src/api/images.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export function pageHashKnownThumbnailUrl(hash?: string): string | undefined {
|
||||
if (hash) return `${import.meta.env.VITE_KOMGA_API_URL}/api/v1/page-hashes/${hash}/thumbnail`
|
||||
return undefined
|
||||
}
|
||||
144
next-ui/src/assets/cover.svg
Normal file
144
next-ui/src/assets/cover.svg
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
height="297mm"
|
||||
viewBox="0 0 793.70081 1122.5197"
|
||||
width="210mm"
|
||||
version="1.1"
|
||||
id="svg4586"
|
||||
sodipodi:docname="cover.svg"
|
||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
||||
<metadata
|
||||
id="metadata4592">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs4590">
|
||||
<linearGradient
|
||||
id="linearGradient6082"
|
||||
osb:paint="solid">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop6080"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="linearGradient6076"
|
||||
osb:paint="solid">
|
||||
<stop
|
||||
style="stop-color:#000000;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop6074"/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
inkscape:collect="always"
|
||||
xlink:href="#linearGradient6082"
|
||||
id="linearGradient6084"
|
||||
x1="77.866814"
|
||||
y1="386.00677"
|
||||
x2="217.20259"
|
||||
y2="386.00677"
|
||||
gradientUnits="userSpaceOnUse"/>
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Greyscale"
|
||||
id="filter4541">
|
||||
<feColorMatrix
|
||||
values="0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0 0 0 1 0 "
|
||||
id="feColorMatrix4539"/>
|
||||
</filter>
|
||||
<filter
|
||||
style="color-interpolation-filters:sRGB"
|
||||
inkscape:label="Greyscale"
|
||||
id="filter4545">
|
||||
<feColorMatrix
|
||||
values="0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0 0 0 1 0 "
|
||||
id="feColorMatrix4543"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1656"
|
||||
inkscape:window-height="1368"
|
||||
id="namedview4588"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.31281188"
|
||||
inkscape:cx="546.28827"
|
||||
inkscape:cy="1456.7169"
|
||||
inkscape:window-x="-7"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg4586"
|
||||
inkscape:snap-page="true"
|
||||
units="mm"
|
||||
showborder="true"
|
||||
viewbox-width="1"
|
||||
viewbox-height="1"
|
||||
scale-x="0.60001"/>
|
||||
<rect
|
||||
style="fill:#005ed3;fill-opacity:1;stroke:none;stroke-width:2.21274972;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4541)"
|
||||
id="rect826"
|
||||
width="595.27557"
|
||||
height="841.88977"
|
||||
x="1.7763568e-14"
|
||||
y="-2.3789293e-05"
|
||||
transform="matrix(1.3333334,0,0,1.3333333,0,7.9297629e-6)"/>
|
||||
<g
|
||||
id="g4537"
|
||||
transform="matrix(1.3333334,0,0,1.3333333,23.517062,16.073517)"
|
||||
style="filter:url(#filter4545)">
|
||||
<path
|
||||
style="fill:#ff0335"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4560"
|
||||
d="m 280,239.63194 37.10937,63.73828 70.57422,-31.41406 -10.52734,71.71875 77.07812,12.91015 -54.14453,52.30469 54.14453,52.30469 -77.07812,12.91015 10.52734,71.71875 L 317.10937,514.40928 280,578.14756 l -37.10938,-63.73828 -70.57421,31.41406 10.52734,-71.71875 -77.07813,-12.91015 54.14454,-52.30469 -54.14454,-52.30469 77.07813,-12.91015 -10.52734,-71.71875 70.57421,31.41406 z m 0,0"/>
|
||||
<path
|
||||
style="fill:#c2001b"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4562"
|
||||
d="m 454.23047,461.19053 -77.07031,12.91016 10.51953,71.71875 L 317.10938,514.40928 280,578.15147 V 239.62803 l 37.10938,63.74219 70.57031,-31.41016 -6.75781,46.10156 -3.76172,25.61719 58.80078,9.85156 18.26953,3.0586 -13.39063,12.92969 -40.75,39.37109 11.37891,10.98828 z m 0,0"/>
|
||||
<path
|
||||
style="fill:#ffdf47"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4564"
|
||||
d="M 280,607.95616 236.69531,533.58506 153.51953,570.6085 165.86719,486.46787 73.949219,471.07334 138.32031,408.88975 73.949219,346.70616 165.86719,331.31162 153.51953,247.171 236.69922,284.19444 280,209.82335 l 43.30469,74.37109 83.17578,-37.02344 -12.34766,84.14062 91.91797,15.39454 -64.37109,62.18359 64.37109,62.18359 -91.91797,15.39844 12.34766,84.13672 -83.17578,-37.02344 z M 249.08203,495.2335 280,548.33506 310.91797,495.2335 368.88281,521.03428 360.17969,461.74131 422.41797,451.31553 378.5,408.88975 422.41797,366.46397 360.17969,356.03819 368.88281,296.74522 310.91797,322.546 280,269.44444 249.08203,322.546 l -57.96484,-25.80078 8.70312,59.29297 -62.23828,10.42578 43.91797,42.42578 -43.91797,42.42578 62.23828,10.42578 -8.70312,59.29297 z m 0,0"/>
|
||||
<path
|
||||
style="fill:#fec000"
|
||||
inkscape:connector-curvature="0"
|
||||
id="path4566"
|
||||
d="m 427.30859,414.33116 -5.6289,-5.44141 25.16015,-24.30078 39.21094,-37.87891 -55.75,-9.33984 -36.17187,-6.0586 2.80078,-19.09375 9.55078,-65.04687 -83.17969,37.01953 L 280,209.81944 v 59.62109 l 30.92188,53.10938 57.95703,-25.8086 -3.91016,26.66797 -2.54687,17.37891 -2.24219,15.25 2.48047,0.42187 59.76172,10.00781 -43.92188,42.42188 16.96875,16.39062 26.95313,26.03125 -62.24219,10.42969 8.69922,59.29688 -57.95703,-25.8086 L 280,548.33897 v 59.62109 l 43.30078,-74.37109 83.17969,37.01953 -12.35156,-84.14063 91.92187,-15.39843 z m 0,0"/>
|
||||
<g
|
||||
id="text4596"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.55969238px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,24,152.88975)"
|
||||
aria-label="K">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path824"
|
||||
style="font-size:296.55969238px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.7 KiB |
22
next-ui/src/colada/books.ts
Normal file
22
next-ui/src/colada/books.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { defineQueryOptions } from '@pinia/colada'
|
||||
import { komgaClient } from '@/api/komga-client'
|
||||
|
||||
export const QUERY_KEYS_BOOKS = {
|
||||
root: ['books'] as const,
|
||||
byId: (bookId: string) => [...QUERY_KEYS_BOOKS.root, bookId] as const,
|
||||
}
|
||||
|
||||
export const bookDetailQuery = defineQueryOptions(({ bookId }: { bookId: string }) => ({
|
||||
key: QUERY_KEYS_BOOKS.byId(bookId),
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v1/books/{bookId}', {
|
||||
params: {
|
||||
path: {
|
||||
bookId: bookId,
|
||||
},
|
||||
},
|
||||
})
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
}))
|
||||
18
next-ui/src/colada/history.ts
Normal file
18
next-ui/src/colada/history.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { defineQueryOptions } from '@pinia/colada'
|
||||
import { komgaClient } from '@/api/komga-client'
|
||||
|
||||
export const historyQuery = defineQueryOptions(
|
||||
({ page, size, sort }: { page?: number; size?: number; sort?: string[] }) => ({
|
||||
key: ['history', { page: page, size: size, sort: sort }],
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v1/history', {
|
||||
params: {
|
||||
query: { page: page, size: size, sort: sort },
|
||||
},
|
||||
})
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
placeholderData: (previousData: any) => previousData, // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
}),
|
||||
)
|
||||
22
next-ui/src/colada/series.ts
Normal file
22
next-ui/src/colada/series.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { defineQueryOptions } from '@pinia/colada'
|
||||
import { komgaClient } from '@/api/komga-client'
|
||||
|
||||
export const QUERY_KEYS_SERIES = {
|
||||
root: ['series'] as const,
|
||||
byId: (seriesId: string) => [...QUERY_KEYS_SERIES.root, seriesId] as const,
|
||||
}
|
||||
|
||||
export const seriesDetailQuery = defineQueryOptions(({ seriesId }: { seriesId: string }) => ({
|
||||
key: QUERY_KEYS_SERIES.byId(seriesId),
|
||||
query: () =>
|
||||
komgaClient
|
||||
.GET('/api/v1/series/{seriesId}', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
})
|
||||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
}))
|
||||
9
next-ui/src/components.d.ts
vendored
9
next-ui/src/components.d.ts
vendored
|
|
@ -22,6 +22,15 @@ declare module 'vue' {
|
|||
FragmentBuildVersion: typeof import('./fragments/fragment/BuildVersion.vue')['default']
|
||||
FragmentDialogConfirm: typeof import('./fragments/fragment/dialog/Confirm.vue')['default']
|
||||
FragmentDialogConfirmEdit: typeof import('./fragments/fragment/dialog/ConfirmEdit.vue')['default']
|
||||
FragmentHistoryBookFileDeleted: typeof import('./fragments/fragment/history/BookFileDeleted.vue')['default']
|
||||
FragmentHistoryExpandBookConverted: typeof import('./fragments/fragment/history/expand/BookConverted.vue')['default']
|
||||
FragmentHistoryExpandBookFileDeleted: typeof import('./fragments/fragment/history/expand/BookFileDeleted.vue')['default']
|
||||
FragmentHistoryExpandBookImported: typeof import('./fragments/fragment/history/expand/BookImported.vue')['default']
|
||||
FragmentHistoryExpandDuplicatePageDeleted: typeof import('./fragments/fragment/history/expand/DuplicatePageDeleted.vue')['default']
|
||||
FragmentHistoryExpandFileDeleted: typeof import('./fragments/fragment/history/ExpandFileDeleted.vue')['default']
|
||||
FragmentHistoryExpandSeriesFolderDeleted: typeof import('./fragments/fragment/history/expand/SeriesFolderDeleted.vue')['default']
|
||||
FragmentHistoryExpandTable: typeof import('./fragments/fragment/history/expand/Table.vue')['default']
|
||||
FragmentHistoryTable: typeof import('./fragments/fragment/history/Table.vue')['default']
|
||||
FragmentLocaleSelector: typeof import('./fragments/fragment/LocaleSelector.vue')['default']
|
||||
FragmentSnackQueue: typeof import('./fragments/fragment/SnackQueue.vue')['default']
|
||||
FragmentThemeSelector: typeof import('./fragments/fragment/ThemeSelector.vue')['default']
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@ import * as Stories from './Table.stories';
|
|||
|
||||
<Meta of={Stories} />
|
||||
|
||||
# ApikeyTable
|
||||
# FragmentApikeyTable
|
||||
|
||||
Table showing API keys.
|
||||
[Server DataTable](https://vuetifyjs.com/en/components/data-tables/server-side-tables/) showing API keys.
|
||||
|
||||
<Canvas of={Stories.Default} />
|
||||
|
|
|
|||
13
next-ui/src/fragments/fragment/history/Table.mdx
Normal file
13
next-ui/src/fragments/fragment/history/Table.mdx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import * as Stories from './Table.stories';
|
||||
|
||||
<Meta of={Stories} />
|
||||
|
||||
# FragmentHistoryTable
|
||||
|
||||
[Server DataTable](https://vuetifyjs.com/en/components/data-tables/server-side-tables/) showing historical events:
|
||||
- rows can be expanded to display event details
|
||||
- Series and Book column will show the actual series/book name if it exists
|
||||
|
||||
<Canvas of={Stories.Default} />
|
||||
58
next-ui/src/fragments/fragment/history/Table.stories.ts
Normal file
58
next-ui/src/fragments/fragment/history/Table.stories.ts
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import Table from './Table.vue'
|
||||
import { delay, http } from 'msw'
|
||||
import { response401Unauthorized } from '@/mocks/api/handlers'
|
||||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { mockPage } from '@/mocks/api/pageable'
|
||||
import { PageRequest } from '@/types/PageRequest'
|
||||
|
||||
const meta = {
|
||||
component: Table,
|
||||
render: (args: object) => ({
|
||||
components: { Table },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Table v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof Table>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.all('*', async () => await delay(5_000))],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const NoData: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v1/history', ({ response }) =>
|
||||
response(200).json(mockPage([], new PageRequest())),
|
||||
),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Error: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.all('*', response401Unauthorized)],
|
||||
},
|
||||
},
|
||||
}
|
||||
232
next-ui/src/fragments/fragment/history/Table.vue
Normal file
232
next-ui/src/fragments/fragment/history/Table.vue
Normal file
|
|
@ -0,0 +1,232 @@
|
|||
<template>
|
||||
<v-data-table-server
|
||||
v-model:sort-by="sortBy"
|
||||
:loading="isLoading"
|
||||
:items="data?.content"
|
||||
:items-length="data?.totalElements || 0"
|
||||
:items-per-page-options="[
|
||||
{ value: 10, title: '10' },
|
||||
{ value: 25, title: '25' },
|
||||
{ value: 50, title: '50' },
|
||||
{ value: 100, title: '100' },
|
||||
]"
|
||||
:headers="headers"
|
||||
item-value="timestamp"
|
||||
fixed-header
|
||||
fixed-footer
|
||||
multi-sort
|
||||
mobile-breakpoint="md"
|
||||
show-expand
|
||||
style="height: 100%"
|
||||
@update:options="updateOptions"
|
||||
>
|
||||
<template #top>
|
||||
<v-toolbar flat>
|
||||
<v-toolbar-title>
|
||||
<v-icon
|
||||
color="medium-emphasis"
|
||||
icon="i-mdi:history"
|
||||
size="x-small"
|
||||
start
|
||||
/>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'History table global header',
|
||||
defaultMessage: 'History',
|
||||
id: '7z91nm',
|
||||
})
|
||||
}}
|
||||
</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
</template>
|
||||
|
||||
<template #no-data>
|
||||
<EmptyStateNetworkError v-if="error" />
|
||||
<template v-else>
|
||||
{{
|
||||
$formatMessage({
|
||||
description: 'History table: shown when table has no data',
|
||||
defaultMessage: 'No recent events',
|
||||
id: 'Ym48fa',
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<template #[`item.type`]="{ value }">
|
||||
<div class="d-flex flex-row-reverse flex-md-row ga-2">
|
||||
<v-icon :icon="getIcon(value)" />
|
||||
<span>{{ $formatMessage(getEventMessage(value)) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #[`item.seriesId`]="{ value: seriesId }">
|
||||
{{ seriesCache[seriesId] || seriesId }}
|
||||
</template>
|
||||
|
||||
<template #[`item.bookId`]="{ value: bookId }">
|
||||
{{ booksCache[bookId] || bookId }}
|
||||
</template>
|
||||
|
||||
<template #[`item.timestamp`]="{ value }">
|
||||
{{ $formatDate(value, { dateStyle: 'medium', timeStyle: 'short' }) }}
|
||||
</template>
|
||||
|
||||
<template #expanded-row="{ columns, item }">
|
||||
<tr>
|
||||
<td
|
||||
:colspan="columns.length"
|
||||
class="py-2 px-1 px-md-4"
|
||||
>
|
||||
<component
|
||||
:is="getExpandedComponent(item.type)"
|
||||
:event="item"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table-server>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { PageRequest, type SortItem } from '@/types/PageRequest'
|
||||
import { useQuery } from '@pinia/colada'
|
||||
import { historyQuery } from '@/colada/history'
|
||||
import FragmentHistoryExpandBookFileDeleted from '@/fragments/fragment/history/expand/BookFileDeleted.vue'
|
||||
import FragmentHistoryExpandBookImported from '@/fragments/fragment/history/expand/BookImported.vue'
|
||||
import FragmentHistoryExpandBookConverted from '@/fragments/fragment/history/expand/BookConverted.vue'
|
||||
import FragmentHistoryExpandDuplicatePageDeleted from '@/fragments/fragment/history/expand/DuplicatePageDeleted.vue'
|
||||
import FragmentHistoryExpandSeriesFolderDeleted from '@/fragments/fragment/history/expand/SeriesFolderDeleted.vue'
|
||||
import { historicalEventMessages } from '@/utils/i18n/enum/historical-event'
|
||||
import type { MessageDescriptor } from '@formatjs/intl/src/types'
|
||||
import { seriesDetailQuery } from '@/colada/series'
|
||||
import { bookDetailQuery } from '@/colada/books'
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
const sortBy = ref<SortItem[]>([{ key: 'timestamp', order: 'desc' }])
|
||||
|
||||
const headers = [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'History Table table header: Event type',
|
||||
defaultMessage: 'Type',
|
||||
id: '6nCIPn',
|
||||
}),
|
||||
key: 'type',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'History Table table header: Series',
|
||||
defaultMessage: 'Series',
|
||||
id: 'pR3VdQ',
|
||||
}),
|
||||
key: 'seriesId',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'History Table table header: Book',
|
||||
defaultMessage: 'Book',
|
||||
id: 'zqAxRE',
|
||||
}),
|
||||
key: 'bookId',
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'History Table table header: Date Time',
|
||||
defaultMessage: 'Date Time',
|
||||
id: 'HMbMJF',
|
||||
}),
|
||||
key: 'timestamp',
|
||||
},
|
||||
]
|
||||
|
||||
const pageRequest = ref<PageRequest>(new PageRequest())
|
||||
|
||||
const { data, isLoading, error } = useQuery(historyQuery, () => ({ ...pageRequest.value }))
|
||||
|
||||
function updateOptions({
|
||||
page,
|
||||
itemsPerPage,
|
||||
sortBy,
|
||||
}: {
|
||||
page: number
|
||||
itemsPerPage: number
|
||||
sortBy: SortItem[]
|
||||
}) {
|
||||
pageRequest.value = PageRequest.FromVuetify(page - 1, itemsPerPage, sortBy)
|
||||
}
|
||||
|
||||
function getIcon(eventType: string): string {
|
||||
switch (eventType) {
|
||||
case 'BookFileDeleted':
|
||||
return 'i-mdi:file-remove'
|
||||
case 'SeriesFolderDeleted':
|
||||
return 'i-mdi:folder-remove'
|
||||
case 'DuplicatePageDeleted':
|
||||
return 'i-mdi:book-minus'
|
||||
case 'BookConverted':
|
||||
return 'i-mdi:archive-refresh'
|
||||
case 'BookImported':
|
||||
return 'i-mdi:import'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function getEventMessage(eventType: string): MessageDescriptor {
|
||||
return (
|
||||
historicalEventMessages[eventType] || {
|
||||
id: eventType,
|
||||
defaultMessage: eventType,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function getExpandedComponent(eventType: string): Component | null {
|
||||
switch (eventType) {
|
||||
case 'BookFileDeleted':
|
||||
return markRaw(FragmentHistoryExpandBookFileDeleted)
|
||||
case 'SeriesFolderDeleted':
|
||||
return markRaw(FragmentHistoryExpandSeriesFolderDeleted)
|
||||
case 'DuplicatePageDeleted':
|
||||
return markRaw(FragmentHistoryExpandDuplicatePageDeleted)
|
||||
case 'BookConverted':
|
||||
return markRaw(FragmentHistoryExpandBookConverted)
|
||||
case 'BookImported':
|
||||
return markRaw(FragmentHistoryExpandBookImported)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const seriesCache = reactive<Record<string, string>>({})
|
||||
const seriesCacheNotFound = reactive<string[]>([])
|
||||
const booksCache = reactive<Record<string, string>>({})
|
||||
const booksCacheNotFound = reactive<string[]>([])
|
||||
|
||||
watch(data, (data) => {
|
||||
for (const seriesId of new Set(data?.content?.map((s) => s.seriesId))) {
|
||||
if (seriesId && !seriesCacheNotFound.includes(seriesId) && !(seriesId in seriesCache)) {
|
||||
const { refresh } = useQuery(seriesDetailQuery, () => ({ seriesId: seriesId }))
|
||||
refresh(true)
|
||||
.then(({ data }) => {
|
||||
if (data) seriesCache[seriesId] = data.metadata.title
|
||||
})
|
||||
.catch(() => seriesCacheNotFound.push(seriesId))
|
||||
}
|
||||
}
|
||||
|
||||
for (const bookId of new Set(data?.content?.map((s) => s.bookId))) {
|
||||
if (bookId && !booksCacheNotFound.includes(bookId) && !(bookId in booksCache)) {
|
||||
const { refresh } = useQuery(bookDetailQuery, () => ({ bookId: bookId }))
|
||||
refresh(true)
|
||||
.then(({ data }) => {
|
||||
if (data) booksCache[bookId] = data.metadata.title
|
||||
})
|
||||
.catch(() => booksCacheNotFound.push(bookId))
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import BookConverted from './BookConverted.vue'
|
||||
import { historyBookConverted } from '@/mocks/api/handlers/history'
|
||||
|
||||
const meta = {
|
||||
component: BookConverted,
|
||||
render: (args: object) => ({
|
||||
components: { BookConverted },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<BookConverted v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof BookConverted>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
event: historyBookConverted,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<fragment-history-expand-table :rows="rows" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
const { event } = defineProps<{
|
||||
event: components['schemas']['HistoricalEventDto']
|
||||
}>()
|
||||
|
||||
const rows = computed(() => [
|
||||
{ header: 'Before', value: event.properties['former file'] },
|
||||
{ header: 'After', value: event.properties.name },
|
||||
])
|
||||
</script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import BookFileDeleted from './BookFileDeleted.vue'
|
||||
import { historyBookFileDeleted } from '@/mocks/api/handlers/history'
|
||||
|
||||
const meta = {
|
||||
component: BookFileDeleted,
|
||||
render: (args: object) => ({
|
||||
components: { BookFileDeleted },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<BookFileDeleted v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof BookFileDeleted>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
event: historyBookFileDeleted,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<fragment-history-expand-table :rows="rows" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
const { event } = defineProps<{
|
||||
event: components['schemas']['HistoricalEventDto']
|
||||
}>()
|
||||
|
||||
const rows = computed(() => [
|
||||
{ header: 'File', value: event.properties.name },
|
||||
{ header: 'Reason', value: event.properties.reason },
|
||||
])
|
||||
</script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import BookImported from './BookImported.vue'
|
||||
import { historyBookImported } from '@/mocks/api/handlers/history'
|
||||
|
||||
const meta = {
|
||||
component: BookImported,
|
||||
render: (args: object) => ({
|
||||
components: { BookImported },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<BookImported v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof BookImported>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
event: historyBookImported,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<fragment-history-expand-table :rows="rows" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
const { event } = defineProps<{
|
||||
event: components['schemas']['HistoricalEventDto']
|
||||
}>()
|
||||
|
||||
const rows = computed(() => [
|
||||
{ header: 'File', value: event.properties.name },
|
||||
{ header: 'Source file', value: event.properties.source },
|
||||
{ header: 'Upgrade', value: event.properties.upgrade },
|
||||
])
|
||||
</script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DuplicatePageDeleted from './DuplicatePageDeleted.vue'
|
||||
import { historyDuplicatePageDeleted } from '@/mocks/api/handlers/history'
|
||||
|
||||
const meta = {
|
||||
component: DuplicatePageDeleted,
|
||||
render: (args: object) => ({
|
||||
components: { DuplicatePageDeleted },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<DuplicatePageDeleted v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof DuplicatePageDeleted>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
event: historyDuplicatePageDeleted,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<v-sheet
|
||||
rounded="lg"
|
||||
border
|
||||
>
|
||||
<div class="d-flex flex-column flex-md-row align-center">
|
||||
<v-table
|
||||
density="compact"
|
||||
class="my-2"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Book file</th>
|
||||
<td>{{ event.properties.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Page file name</th>
|
||||
<td>{{ event.properties['page file name'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Page number</th>
|
||||
<td>{{ event.properties['page number'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Page file size</th>
|
||||
<td>{{ event.properties['page file size'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Page media type</th>
|
||||
<td>{{ event.properties['page media type'] }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
||||
<!-- TODO: add link to known page hash -->
|
||||
<v-img
|
||||
width="200"
|
||||
height="200"
|
||||
contain
|
||||
:src="pageHashKnownThumbnailUrl(event.properties['page file hash'])"
|
||||
lazy-src="@/assets/cover.svg"
|
||||
class="my-2"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height">
|
||||
<v-progress-circular
|
||||
color="grey"
|
||||
indeterminate
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { pageHashKnownThumbnailUrl } from '@/api/images'
|
||||
|
||||
const { event } = defineProps<{
|
||||
event: components['schemas']['HistoricalEventDto']
|
||||
}>()
|
||||
</script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SeriesFolderDeleted from './SeriesFolderDeleted.vue'
|
||||
import { historySeriesFolderDeleted } from '@/mocks/api/handlers/history'
|
||||
|
||||
const meta = {
|
||||
component: SeriesFolderDeleted,
|
||||
render: (args: object) => ({
|
||||
components: { SeriesFolderDeleted },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<SeriesFolderDeleted v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof SeriesFolderDeleted>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
event: historySeriesFolderDeleted,
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<template>
|
||||
<fragment-history-expand-table :rows="rows" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
const { event } = defineProps<{
|
||||
event: components['schemas']['HistoricalEventDto']
|
||||
}>()
|
||||
|
||||
const rows = computed(() => [
|
||||
{ header: 'Folder', value: event.properties.name },
|
||||
{ header: 'Reason', value: event.properties.reason },
|
||||
])
|
||||
</script>
|
||||
24
next-ui/src/fragments/fragment/history/expand/Table.vue
Normal file
24
next-ui/src/fragments/fragment/history/expand/Table.vue
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<v-sheet
|
||||
rounded="lg"
|
||||
border
|
||||
>
|
||||
<v-table density="compact">
|
||||
<tbody
|
||||
v-for="row in rows"
|
||||
:key="row.header"
|
||||
>
|
||||
<tr>
|
||||
<th>{{ row.header }}</th>
|
||||
<td>{{ row.value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-sheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { rows } = defineProps<{
|
||||
rows: { header: string; value?: string }[]
|
||||
}>()
|
||||
</script>
|
||||
18
next-ui/src/generated/openapi/komga.d.ts
vendored
18
next-ui/src/generated/openapi/komga.d.ts
vendored
|
|
@ -3814,6 +3814,10 @@ export interface components {
|
|||
SettingsUpdateDto: {
|
||||
deleteEmptyCollections?: boolean;
|
||||
deleteEmptyReadLists?: boolean;
|
||||
/**
|
||||
* @deprecated
|
||||
* @description Will be removed in a future version
|
||||
*/
|
||||
kepubifyPath?: string;
|
||||
/** Format: int32 */
|
||||
koboPort?: number;
|
||||
|
|
@ -4641,6 +4645,13 @@ export interface operations {
|
|||
"*/*": components["schemas"]["ValidationErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Not Found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
bookAnalyze: {
|
||||
|
|
@ -8133,6 +8144,13 @@ export interface operations {
|
|||
"*/*": components["schemas"]["ValidationErrorResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Not Found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
seriesAnalyze: {
|
||||
|
|
|
|||
|
|
@ -7,18 +7,26 @@ import { referentialHandlers } from '@/mocks/api/handlers/referential'
|
|||
import { usersHandlers } from '@/mocks/api/handlers/users'
|
||||
import { settingsHandlers } from '@/mocks/api/handlers/settings'
|
||||
import { claimHandlers } from '@/mocks/api/handlers/claim'
|
||||
import { historyHandlers } from '@/mocks/api/handlers/history'
|
||||
import { seriesHandlers } from '@/mocks/api/handlers/series'
|
||||
import { booksHandlers } from '@/mocks/api/handlers/books'
|
||||
|
||||
export const handlers = [
|
||||
...actuatorHandlers,
|
||||
...announcementHandlers,
|
||||
...booksHandlers,
|
||||
...claimHandlers,
|
||||
...historyHandlers,
|
||||
...librariesHandlers,
|
||||
...referentialHandlers,
|
||||
...releasesHandlers,
|
||||
...seriesHandlers,
|
||||
...settingsHandlers,
|
||||
...usersHandlers,
|
||||
]
|
||||
|
||||
export const response404NotFound = () => HttpResponse.json({ error: 'NotFound' }, { status: 404 })
|
||||
|
||||
export const response401Unauthorized = () =>
|
||||
HttpResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
|
||||
|
|
|
|||
67
next-ui/src/mocks/api/handlers/books.ts
Normal file
67
next-ui/src/mocks/api/handlers/books.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
|
||||
const book = {
|
||||
id: '05RKH8CC8B4RW',
|
||||
seriesId: '57',
|
||||
seriesTitle: 'Super Duck',
|
||||
libraryId: '56',
|
||||
name: 'Super_Duck_001__MLJ___Fall_1944___c2c___titansfan_editor_',
|
||||
url: '/books/Super Duck/Super_Duck_001__MLJ___Fall_1944___c2c___titansfan_editor_.cbz',
|
||||
number: 1,
|
||||
created: new Date('2021-07-27T13:33:52Z'),
|
||||
lastModified: new Date('2023-07-22T11:14:47Z'),
|
||||
fileLastModified: new Date('2020-03-05T03:20:12Z'),
|
||||
sizeBytes: 82920938,
|
||||
size: '79.1 MiB',
|
||||
media: {
|
||||
status: 'READY',
|
||||
mediaType: 'application/zip',
|
||||
pagesCount: 53,
|
||||
comment: '',
|
||||
epubDivinaCompatible: false,
|
||||
epubIsKepub: false,
|
||||
mediaProfile: 'DIVINA',
|
||||
},
|
||||
metadata: {
|
||||
title: 'Super Duck 001',
|
||||
titleLock: true,
|
||||
summary: '',
|
||||
summaryLock: false,
|
||||
number: '1',
|
||||
numberLock: false,
|
||||
numberSort: 1.0,
|
||||
numberSortLock: false,
|
||||
releaseDateLock: false,
|
||||
authors: [],
|
||||
authorsLock: true,
|
||||
tags: [],
|
||||
tagsLock: false,
|
||||
isbn: '',
|
||||
isbnLock: false,
|
||||
links: [],
|
||||
linksLock: false,
|
||||
created: new Date('2021-07-27T13:33:52Z'),
|
||||
lastModified: new Date('2023-03-15T09:50:54Z'),
|
||||
},
|
||||
readProgress: {
|
||||
page: 53,
|
||||
completed: true,
|
||||
readDate: new Date('2025-02-19T11:29:25Z'),
|
||||
created: new Date('2025-02-19T11:29:25Z'),
|
||||
lastModified: new Date('2025-02-19T11:29:25Z'),
|
||||
deviceId: '',
|
||||
deviceName: '',
|
||||
},
|
||||
deleted: false,
|
||||
fileHash: '7dc12ae431a8847b7f49918745254b0b',
|
||||
oneshot: false,
|
||||
}
|
||||
|
||||
export const booksHandlers = [
|
||||
httpTyped.get('/api/v1/books/{bookId}', ({ params, response }) => {
|
||||
if (params.bookId === '404') return response(404).empty()
|
||||
return response(200).json(
|
||||
Object.assign({}, book, { metadata: { title: `Book ${params.bookId}` } }),
|
||||
)
|
||||
}),
|
||||
]
|
||||
118
next-ui/src/mocks/api/handlers/history.ts
Normal file
118
next-ui/src/mocks/api/handlers/history.ts
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { mockPage } from '@/mocks/api/pageable'
|
||||
import { PageRequest } from '@/types/PageRequest'
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import logoUrl from '@/assets/logo.svg'
|
||||
|
||||
export const historyBookImported = {
|
||||
type: 'BookImported',
|
||||
timestamp: new Date('2025-08-07T09:36:22.596'),
|
||||
bookId: 'B5',
|
||||
seriesId: 'S1',
|
||||
properties: {
|
||||
name: '/data/Books/Donjon Parade/Donjon Parade - 09 - Nécomancien pour de faux.cbz',
|
||||
source: '/data/Import/Donjon Parade - 9 - Nécomancien pour de faux (Sfar-Trandheim-Delaf).cbz',
|
||||
upgrade: 'No',
|
||||
},
|
||||
}
|
||||
|
||||
export const historySeriesFolderDeleted = {
|
||||
type: 'SeriesFolderDeleted',
|
||||
timestamp: new Date('2025-08-05T21:33:56.452'),
|
||||
seriesId: '404',
|
||||
properties: {
|
||||
name: '/data/Books/One Piece',
|
||||
reason: 'Folder was deleted because it was empty',
|
||||
},
|
||||
}
|
||||
|
||||
export const historyBookFileDeleted = {
|
||||
type: 'BookFileDeleted',
|
||||
timestamp: new Date('2025-08-05T21:33:56.445'),
|
||||
bookId: '404',
|
||||
seriesId: 'S2',
|
||||
properties: {
|
||||
name: '/data/Books/One Piece/Volume 1.cbz',
|
||||
reason: 'File was deleted by user request',
|
||||
},
|
||||
}
|
||||
|
||||
export const historyBookImportedForUpgrade = {
|
||||
type: 'BookImported',
|
||||
timestamp: new Date('2025-07-28T17:52:14.126'),
|
||||
bookId: 'B15',
|
||||
seriesId: 'S3',
|
||||
properties: {
|
||||
name: '/data/Lastman/Lastman - T12.cbz',
|
||||
source: '/data/Import/Lastman/Lastman_-_Nouvelle_Édition_T12.cbz',
|
||||
upgrade: 'Yes',
|
||||
},
|
||||
}
|
||||
|
||||
export const historyBookFileDeletedForUpgrade = {
|
||||
type: 'BookFileDeleted',
|
||||
timestamp: new Date('2025-07-28T17:52:14.016'),
|
||||
bookId: 'b14',
|
||||
seriesId: 'S3',
|
||||
properties: {
|
||||
name: '/data/Lastman/Lastman - 12.cbz',
|
||||
reason: 'File was deleted to import an upgrade',
|
||||
},
|
||||
}
|
||||
|
||||
export const historyDuplicatePageDeleted = {
|
||||
type: 'DuplicatePageDeleted',
|
||||
timestamp: new Date('2025-07-26T21:33:18.809'),
|
||||
bookId: 'B5',
|
||||
seriesId: 'S2',
|
||||
properties: {
|
||||
name: '/data/Lastman/Volume 6.cbz',
|
||||
'page file hash': 'e3f8f3814609645d6ffc6f6f2c4c65aa',
|
||||
'page file name': 'credits.jpg',
|
||||
'page file size': '1204289',
|
||||
'page media type': 'image/jpeg',
|
||||
'page number': '34',
|
||||
},
|
||||
}
|
||||
|
||||
export const historyBookConverted = {
|
||||
type: 'BookConverted',
|
||||
timestamp: new Date('2025-07-07T17:33:39.545'),
|
||||
bookId: 'B25',
|
||||
seriesId: 'S21',
|
||||
properties: {
|
||||
'former file': '/data/Usagi Yojimbo/Usagi Yojimbo (Book 40) - The Crow (2025).cbr',
|
||||
name: '/data/Usagi Yojimbo/Usagi Yojimbo (Book 40) - The Crow (2025).cbz',
|
||||
},
|
||||
}
|
||||
|
||||
const history = [
|
||||
historyBookImported,
|
||||
historySeriesFolderDeleted,
|
||||
historyBookFileDeleted,
|
||||
historyBookFileDeletedForUpgrade,
|
||||
historyBookImportedForUpgrade,
|
||||
historyDuplicatePageDeleted,
|
||||
historyBookConverted,
|
||||
]
|
||||
|
||||
export const historyHandlers = [
|
||||
httpTyped.get('/api/v1/history', ({ query, response }) =>
|
||||
response(200).json(
|
||||
mockPage(
|
||||
history,
|
||||
new PageRequest(Number(query.get('page')), Number(query.get('size')), query.getAll('sort')),
|
||||
),
|
||||
),
|
||||
),
|
||||
http.get('*/api/v1/page-hashes/*/thumbnail', async () => {
|
||||
// Get an ArrayBuffer from reading the file from disk or fetching it.
|
||||
const buffer = await fetch(logoUrl).then((response) => response.arrayBuffer())
|
||||
|
||||
return HttpResponse.arrayBuffer(buffer, {
|
||||
headers: {
|
||||
'content-type': 'image/svg+xml',
|
||||
},
|
||||
})
|
||||
}),
|
||||
]
|
||||
66
next-ui/src/mocks/api/handlers/series.ts
Normal file
66
next-ui/src/mocks/api/handlers/series.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
|
||||
const series = {
|
||||
id: '57',
|
||||
libraryId: '56',
|
||||
name: 'Super Duck',
|
||||
url: '/books/Super Duck',
|
||||
created: new Date('2020-07-05T12:11:50Z'),
|
||||
lastModified: new Date('2021-07-27T13:33:54Z'),
|
||||
fileLastModified: new Date('2020-03-05T15:24:59Z'),
|
||||
booksCount: 5,
|
||||
booksReadCount: 1,
|
||||
booksUnreadCount: 3,
|
||||
booksInProgressCount: 1,
|
||||
metadata: {
|
||||
status: 'ENDED',
|
||||
statusLock: true,
|
||||
title: 'Super Duck',
|
||||
titleLock: false,
|
||||
titleSort: 'Super Duck',
|
||||
titleSortLock: false,
|
||||
summary:
|
||||
'Super Duck is the greatest hero of New Duck City. Brash, arrogant and virtually unbeatable, he’s defeated all threats to the city and routinely foils the schemes of his greatest rival, criminal genius and corporate billionaire Dapper Duck.\n\nHowever, when Dapper takes to the streets with a giant mechanical monster, will Super Duck prove once more to be the heroic champion everyone knows and loves or is his goose finally cooked?',
|
||||
summaryLock: true,
|
||||
readingDirection: 'LEFT_TO_RIGHT',
|
||||
readingDirectionLock: true,
|
||||
publisher: 'Archie',
|
||||
publisherLock: true,
|
||||
ageRatingLock: false,
|
||||
language: 'en',
|
||||
languageLock: true,
|
||||
genres: ['humor'],
|
||||
genresLock: true,
|
||||
tags: [],
|
||||
tagsLock: false,
|
||||
totalBookCount: 94,
|
||||
totalBookCountLock: true,
|
||||
sharingLabels: [],
|
||||
sharingLabelsLock: false,
|
||||
links: [],
|
||||
linksLock: false,
|
||||
alternateTitles: [],
|
||||
alternateTitlesLock: false,
|
||||
created: new Date('2020-07-05T12:11:50Z'),
|
||||
lastModified: new Date('2023-07-22T11:14:45Z'),
|
||||
},
|
||||
booksMetadata: {
|
||||
authors: [],
|
||||
tags: [],
|
||||
summary: '',
|
||||
summaryNumber: '',
|
||||
created: new Date('2021-01-11T09:59:23Z'),
|
||||
lastModified: new Date('2023-07-22T11:14:45Z'),
|
||||
},
|
||||
deleted: false,
|
||||
oneshot: false,
|
||||
}
|
||||
|
||||
export const seriesHandlers = [
|
||||
httpTyped.get('/api/v1/series/{seriesId}', ({ params, response }) => {
|
||||
if (params.seriesId === '404') return response(404).empty()
|
||||
return response(200).json(
|
||||
Object.assign({}, series, { metadata: { title: `Series ${params.seriesId}` } }),
|
||||
)
|
||||
}),
|
||||
]
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
<template>
|
||||
<h1>History</h1>
|
||||
<v-container
|
||||
fluid
|
||||
class="pa-0 pa-sm-4 h-100 h-sm-auto"
|
||||
>
|
||||
<fragment-history-table />
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<route lang="yaml">
|
||||
meta:
|
||||
|
|
|
|||
30
next-ui/src/utils/i18n/enum/historical-event.ts
Normal file
30
next-ui/src/utils/i18n/enum/historical-event.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { defineMessage, type MessageDescriptor } from 'vue-intl'
|
||||
|
||||
// the keys are the event names for easier lookup
|
||||
export const historicalEventMessages: Record<string, MessageDescriptor> = {
|
||||
BookFileDeleted: defineMessage({
|
||||
description: 'Historical event: BookFileDeleted',
|
||||
defaultMessage: 'Book File Deleted',
|
||||
id: 'enum.historicalEvent.BookFileDeleted',
|
||||
}),
|
||||
SeriesFolderDeleted: defineMessage({
|
||||
description: 'Historical event: SeriesFolderDeleted',
|
||||
defaultMessage: 'Series Folder Deleted',
|
||||
id: 'enum.historicalEvent.SeriesFolderDeleted',
|
||||
}),
|
||||
DuplicatePageDeleted: defineMessage({
|
||||
description: 'Historical event: DuplicatePageDeleted',
|
||||
defaultMessage: 'Duplicate Page Deleted',
|
||||
id: 'enum.historicalEvent.DuplicatePageDeleted',
|
||||
}),
|
||||
BookConverted: defineMessage({
|
||||
description: 'Historical event: ',
|
||||
defaultMessage: 'Book Converted',
|
||||
id: 'enum.historicalEvent.BookConverted',
|
||||
}),
|
||||
BookImported: defineMessage({
|
||||
description: 'Historical event: BookImported',
|
||||
defaultMessage: 'Book Imported',
|
||||
id: 'enum.historicalEvent.BookImported',
|
||||
}),
|
||||
}
|
||||
Loading…
Reference in a new issue