mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 12:35:30 +02:00
more item card stuff
This commit is contained in:
parent
fa46c73fdb
commit
554309e88c
25 changed files with 1015 additions and 13 deletions
14
next-ui/package-lock.json
generated
14
next-ui/package-lock.json
generated
|
|
@ -12,6 +12,7 @@
|
|||
"@pinia/colada": "^0.21.1",
|
||||
"@pinia/colada-plugin-auto-refetch": "^0.2.4",
|
||||
"@pinia/colada-plugin-delay": "^0.1.4",
|
||||
"@vueuse/components": "^14.1.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/router": "^14.1.0",
|
||||
"core-js": "^3.47.0",
|
||||
|
|
@ -4427,6 +4428,19 @@
|
|||
"vuetify": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/components": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/components/-/components-14.1.0.tgz",
|
||||
"integrity": "sha512-SDRJUAv3H7/PMh+KkYpq0d5KMzpKOfqx4qcV4xyN4mZOLPw8NkiWu+yDcfXwI8h1uCqhRNz2cdeaLa+IuaehFw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vueuse/core": "14.1.0",
|
||||
"@vueuse/shared": "14.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vueuse/core": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vueuse/core/-/core-14.1.0.tgz",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"@pinia/colada": "^0.21.1",
|
||||
"@pinia/colada-plugin-auto-refetch": "^0.2.4",
|
||||
"@pinia/colada-plugin-delay": "^0.1.4",
|
||||
"@vueuse/components": "^14.1.0",
|
||||
"@vueuse/core": "^14.1.0",
|
||||
"@vueuse/router": "^14.1.0",
|
||||
"core-js": "^3.47.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { defineQueryOptions } from '@pinia/colada'
|
||||
import { defineMutation, defineQueryOptions, useMutation } from '@pinia/colada'
|
||||
import { komgaClient } from '@/api/komga-client'
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import type { PageRequest } from '@/types/PageRequest'
|
||||
import { seriesMetadataToDto } from '@/functions/series'
|
||||
|
||||
export const QUERY_KEYS_SERIES = {
|
||||
root: ['series'] as const,
|
||||
|
|
@ -16,7 +17,7 @@ export const seriesListQuery = defineQueryOptions(
|
|||
pageRequest,
|
||||
}: {
|
||||
search: components['schemas']['SeriesSearch']
|
||||
pause: boolean
|
||||
pause?: boolean
|
||||
pageRequest?: PageRequest
|
||||
}) => ({
|
||||
key: QUERY_KEYS_SERIES.bySearch({ search: search, pageRequest: pageRequest }),
|
||||
|
|
@ -51,3 +52,93 @@ export const seriesDetailQuery = defineQueryOptions(({ seriesId }: { seriesId: s
|
|||
// unwrap the openapi-fetch structure on success
|
||||
.then((res) => res.data),
|
||||
}))
|
||||
|
||||
export const useRefreshMetadataSeries = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (seriesId: string) =>
|
||||
komgaClient.POST('/api/v1/series/{seriesId}/metadata/refresh', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useAnalyzeSeries = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (seriesId: string) =>
|
||||
komgaClient.POST('/api/v1/series/{seriesId}/analyze', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useDeleteSeries = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (seriesId: string) =>
|
||||
komgaClient.DELETE('/api/v1/series/{seriesId}/file', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useMarkSeriesRead = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (seriesId: string) =>
|
||||
komgaClient.POST('/api/v1/series/{seriesId}/read-progress', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useMarkSeriesUnread = defineMutation(() =>
|
||||
useMutation({
|
||||
mutation: (seriesId: string) =>
|
||||
komgaClient.DELETE('/api/v1/series/{seriesId}/read-progress', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
export const useUpdateSeriesMetadata = defineMutation(() => {
|
||||
// const queryCache = useQueryCache()
|
||||
return useMutation({
|
||||
mutation: ({
|
||||
seriesId,
|
||||
metadata,
|
||||
}: {
|
||||
seriesId: string
|
||||
metadata: components['schemas']['SeriesMetadataDto']
|
||||
}) =>
|
||||
komgaClient.PATCH('/api/v1/series/{seriesId}/metadata', {
|
||||
params: {
|
||||
path: {
|
||||
seriesId: seriesId,
|
||||
},
|
||||
},
|
||||
body: seriesMetadataToDto(metadata),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
//TODO: check how to invalidate cache
|
||||
// void queryCache.invalidateQueries({ key: QUERY_KEYS_LIBRARIES.root })
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
9
next-ui/src/components.d.ts
vendored
9
next-ui/src/components.d.ts
vendored
|
|
@ -42,8 +42,8 @@ declare module 'vue' {
|
|||
ImportBooksDirectorySelection: typeof import('./components/import/books/DirectorySelection.vue')['default']
|
||||
ImportBooksTransientBooksTable: typeof import('./components/import/books/TransientBooksTable.vue')['default']
|
||||
ImportReadlistTable: typeof import('./components/import/readlist/Table.vue')['default']
|
||||
ItemCard: typeof import('./components/item/Card/ItemCard.vue')['default']
|
||||
ItemCardSeries: typeof import('./components/item/Card/Series.vue')['default']
|
||||
ItemCard: typeof import('./components/item/card/ItemCard.vue')['default']
|
||||
ItemCardSeries: typeof import('./components/item/card/Series.vue')['default']
|
||||
LayoutAppBar: typeof import('./components/layout/app/Bar.vue')['default']
|
||||
LayoutAppDrawer: typeof import('./components/layout/app/drawer/Drawer.vue')['default']
|
||||
LayoutAppDrawerFooter: typeof import('./components/layout/app/drawer/Footer.vue')['default']
|
||||
|
|
@ -77,6 +77,11 @@ declare module 'vue' {
|
|||
RemoteFileList: typeof import('./components/RemoteFileList.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SeriesDeletionWarning: typeof import('./components/series/DeletionWarning.vue')['default']
|
||||
SeriesFormEditMetadata: typeof import('./components/series/form/EditMetadata.vue')['default']
|
||||
SeriesMenu: typeof import('./components/series/menu/SeriesMenu.vue')['default']
|
||||
SeriesMenuBottomSheet: typeof import('./components/series/menu/SeriesMenuBottomSheet.vue')['default']
|
||||
'SeriesMenuBottomSheet.stories': typeof import('./components/series/menu/SeriesMenuBottomSheet.stories.ts')['default']
|
||||
ServerSettings: typeof import('./components/server/Settings.vue')['default']
|
||||
SnackQueue: typeof import('./components/SnackQueue.vue')['default']
|
||||
ThemeSelector: typeof import('./components/ThemeSelector.vue')['default']
|
||||
|
|
|
|||
|
|
@ -94,6 +94,15 @@ export const QuickActionIcon: Story = {
|
|||
},
|
||||
}
|
||||
|
||||
export const MenuIcon: Story = {
|
||||
args: {
|
||||
menuIcon: 'i-mdi:menu',
|
||||
},
|
||||
play: ({ canvas, userEvent }) => {
|
||||
userEvent.hover(canvas.getByRole('img'))
|
||||
},
|
||||
}
|
||||
|
||||
export const SelectableHover: Story = {
|
||||
args: {},
|
||||
play: ({ canvas, userEvent }) => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
<template>
|
||||
<v-card :width="width">
|
||||
<v-card
|
||||
v-on-long-press.prevent="onCardLongPress"
|
||||
:width="width"
|
||||
>
|
||||
<v-hover
|
||||
v-slot="{ props }"
|
||||
v-model="isHovering"
|
||||
|
|
@ -52,7 +55,7 @@
|
|||
>
|
||||
<!-- Top right number / icon -->
|
||||
<v-icon-btn
|
||||
v-if="!disableSelection"
|
||||
v-if="!hideSelection"
|
||||
:icon="
|
||||
isPreSelect && !isHovering
|
||||
? 'i-mdi:checkbox-blank-circle-outline'
|
||||
|
|
@ -89,6 +92,18 @@
|
|||
color="white"
|
||||
class="bottom-0 left-0 position-absolute"
|
||||
@click="emit('clickQuickAction')"
|
||||
@mouseenter="(event: Event) => quickActionMouseEnter(event)"
|
||||
/>
|
||||
|
||||
<!-- Bottom right menu icon -->
|
||||
<v-icon-btn
|
||||
v-if="isHovering && menuIcon && !hideMenu"
|
||||
:icon="menuIcon"
|
||||
variant="plain"
|
||||
color="white"
|
||||
class="bottom-0 right-0 position-absolute"
|
||||
@click="emit('clickMenu')"
|
||||
@mouseenter="(event: Event) => menuMouseEnter(event)"
|
||||
/>
|
||||
</v-overlay>
|
||||
</div>
|
||||
|
|
@ -121,6 +136,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type { ItemCardEmits, ItemCardLine, ItemCardProps, ItemCardTitle } from '@/types/ItemCard'
|
||||
import { vOnLongPress } from '@vueuse/components'
|
||||
import { usePrimaryInput } from '@/composables/device'
|
||||
|
||||
const { isTouchPrimary } = usePrimaryInput()
|
||||
|
||||
function onCardLongPress() {
|
||||
if (isTouchPrimary.value) emit('cardLongPress')
|
||||
}
|
||||
|
||||
const {
|
||||
stretchPoster = true,
|
||||
|
|
@ -129,6 +152,10 @@ const {
|
|||
selected = false,
|
||||
preSelect = false,
|
||||
fabIcon,
|
||||
quickActionIcon,
|
||||
quickActionMouseEnter = () => {},
|
||||
menuIcon,
|
||||
menuMouseEnter = () => {},
|
||||
} = defineProps<
|
||||
ItemCardProps & {
|
||||
/**
|
||||
|
|
@ -165,6 +192,20 @@ const {
|
|||
* Icon displayed in the bottom-left corner.
|
||||
*/
|
||||
quickActionIcon?: string
|
||||
/**
|
||||
* Callback function called when the mouse enters the quick action button.
|
||||
* @param event
|
||||
*/
|
||||
quickActionMouseEnter?: (event: Event) => void
|
||||
/**
|
||||
* Icon displayed in the bottom-right corner.
|
||||
*/
|
||||
menuIcon?: string
|
||||
/**
|
||||
* Callback function called when the mouse enters the menu button.
|
||||
* @param event
|
||||
*/
|
||||
menuMouseEnter?: (event: Event) => void
|
||||
}
|
||||
>()
|
||||
|
||||
|
|
@ -172,19 +213,27 @@ const emit = defineEmits<
|
|||
ItemCardEmits & {
|
||||
clickFab: []
|
||||
clickQuickAction: []
|
||||
clickMenu: []
|
||||
cardLongPress: []
|
||||
}
|
||||
>()
|
||||
|
||||
const isHovering = ref(false)
|
||||
|
||||
const overlayDisabled = computed(() => {
|
||||
return disableSelection && !fabIcon
|
||||
return disableSelection && !fabIcon && !quickActionIcon && !menuIcon
|
||||
})
|
||||
const isPreSelect = computed(() => preSelect && !selected && !disableSelection)
|
||||
const overlayTransparent = computed(() => (selected || isPreSelect.value) && !isHovering.value)
|
||||
const overlayShown = computed(() => isHovering.value || overlayTransparent.value || preSelect)
|
||||
const hideFab = computed(() => selected || isPreSelect.value)
|
||||
const hideQuickAction = computed(() => selected || isPreSelect.value)
|
||||
const overlayTransparent = computed(
|
||||
() => isTouchPrimary.value || ((selected || isPreSelect.value) && !isHovering.value),
|
||||
)
|
||||
const overlayShown = computed(() => isHovering.value || isPreSelect.value || selected)
|
||||
const hideSelection = computed(
|
||||
() => disableSelection || (isTouchPrimary.value && !isPreSelect.value && !selected),
|
||||
)
|
||||
const hideFab = computed(() => selected || isPreSelect.value || isTouchPrimary.value)
|
||||
const hideQuickAction = computed(() => selected || isPreSelect.value || isTouchPrimary.value)
|
||||
const hideMenu = computed(() => selected || isPreSelect.value || isTouchPrimary.value)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ import { mockSeries1 } from '@/mocks/api/handlers/series'
|
|||
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'
|
||||
|
||||
const meta = {
|
||||
component: Series,
|
||||
render: (args: object) => ({
|
||||
components: { Series },
|
||||
components: { Series, DialogConfirmEditInstance, DialogConfirmInstance },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<Series v-bind="args" />',
|
||||
template: '<Series v-bind="args" /><DialogConfirmEditInstance/><DialogConfirmInstance/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
|
|
|
|||
|
|
@ -7,8 +7,23 @@
|
|||
:top-right-icon="isRead ? 'i-mdi:check' : undefined"
|
||||
fab-icon="i-mdi:play"
|
||||
:quick-action-icon="quickActionIcon"
|
||||
:quick-action-mouse-enter="
|
||||
(event: Event) => (editMetadataActivator = event.currentTarget as Element)
|
||||
"
|
||||
:menu-icon="menuIcon"
|
||||
:menu-mouse-enter="(event: Event) => (menuActivator = event.currentTarget as Element)"
|
||||
v-bind="props"
|
||||
@selection="(val) => emit('selection', val)"
|
||||
@click-quick-action="showEditMetadataDialog()"
|
||||
@card-long-press="bottomSheet = true"
|
||||
/>
|
||||
<SeriesMenu
|
||||
:series="series"
|
||||
:activator="menuActivator"
|
||||
/>
|
||||
<SeriesMenuBottomSheet
|
||||
v-model="bottomSheet"
|
||||
:series="series"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
@ -18,6 +33,7 @@ import { seriesThumbnailUrl } from '@/api/images'
|
|||
import { useIntl } from 'vue-intl'
|
||||
import type { ItemCardEmits, ItemCardLine, ItemCardProps, ItemCardTitle } from '@/types/ItemCard'
|
||||
import { useCurrentUser } from '@/colada/users'
|
||||
import { useEditSeriesMetadataDialog } from '@/composables/series'
|
||||
|
||||
const intl = useIntl()
|
||||
|
||||
|
|
@ -28,6 +44,8 @@ const { series, ...props } = defineProps<
|
|||
>()
|
||||
const emit = defineEmits<ItemCardEmits>()
|
||||
|
||||
const bottomSheet = ref(false)
|
||||
|
||||
const isRead = computed(() => series.booksCount === series.booksReadCount)
|
||||
const unreadCount = computed(() =>
|
||||
series.oneshot ? undefined : series.booksUnreadCount + series.booksInProgressCount,
|
||||
|
|
@ -76,4 +94,14 @@ const lines = computed<ItemCardLine[]>(() => {
|
|||
|
||||
const { isAdmin } = useCurrentUser()
|
||||
const quickActionIcon = computed(() => (isAdmin.value ? 'i-mdi:pencil' : undefined))
|
||||
const menuIcon = computed(() => (isAdmin.value ? 'i-mdi:dots-vertical' : undefined))
|
||||
|
||||
const { showDialog: showEditSeriesMetadataDialog, activator: editMetadataActivator } =
|
||||
useEditSeriesMetadataDialog()
|
||||
|
||||
function showEditMetadataDialog() {
|
||||
showEditSeriesMetadataDialog(series)
|
||||
}
|
||||
|
||||
const menuActivator = ref()
|
||||
</script>
|
||||
|
|
|
|||
31
next-ui/src/components/series/DeletionWarning.stories.ts
Normal file
31
next-ui/src/components/series/DeletionWarning.stories.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import DeletionWarning from './DeletionWarning.vue'
|
||||
|
||||
const meta = {
|
||||
component: DeletionWarning,
|
||||
render: (args: object) => ({
|
||||
components: { DeletionWarning },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<DeletionWarning />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
'Warning shown within a confirmation dialog before deleting a particular series.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof DeletionWarning>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
36
next-ui/src/components/series/DeletionWarning.vue
Normal file
36
next-ui/src/components/series/DeletionWarning.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<template>
|
||||
<v-alert
|
||||
type="warning"
|
||||
variant="tonal"
|
||||
class="mb-4"
|
||||
>
|
||||
<FormattedMessage :message-descriptor="message">
|
||||
<template #ul="Content">
|
||||
<ul class="ps-8">
|
||||
<component :is="Content" />
|
||||
</ul>
|
||||
</template>
|
||||
<template #li="Content">
|
||||
<li>
|
||||
<component :is="Content" />
|
||||
</li>
|
||||
</template>
|
||||
<template #b="Content">
|
||||
<div class="font-weight-bold mt-4">
|
||||
<component :is="Content" />
|
||||
</div>
|
||||
</template>
|
||||
</FormattedMessage>
|
||||
</v-alert>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineMessage } from 'vue-intl'
|
||||
|
||||
const message = defineMessage({
|
||||
description: 'Series files deletion warning notice',
|
||||
defaultMessage:
|
||||
'The series files will be deleted<ul><li>Book media files will be deleted from disk.</li><li>Series and books will de deleted, along with their metadata and read progress.</li></ul><b>This action cannot be undone.</b>',
|
||||
id: 'dfslqC',
|
||||
})
|
||||
</script>
|
||||
30
next-ui/src/components/series/form/EditMetadata.stories.ts
Normal file
30
next-ui/src/components/series/form/EditMetadata.stories.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import CreateEdit from './EditMetadata.vue'
|
||||
|
||||
const meta = {
|
||||
component: CreateEdit,
|
||||
render: (args: object) => ({
|
||||
components: { CreateEdit },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<CreateEdit :model-value="args.modelValue" v-bind="args"/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {},
|
||||
} satisfies Meta<typeof CreateEdit>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
5
next-ui/src/components/series/form/EditMetadata.vue
Normal file
5
next-ui/src/components/series/form/EditMetadata.vue
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<template>
|
||||
<EmptyStateConstruction />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
9
next-ui/src/components/series/menu/SeriesMenu.mdx
Normal file
9
next-ui/src/components/series/menu/SeriesMenu.mdx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import * as Stories from './SeriesMenu.stories.ts';
|
||||
|
||||
<Meta of={Stories} />
|
||||
|
||||
# SeriesMenu
|
||||
|
||||
Action menu for series.
|
||||
80
next-ui/src/components/series/menu/SeriesMenu.stories.ts
Normal file
80
next-ui/src/components/series/menu/SeriesMenu.stories.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SeriesMenu from './SeriesMenu.vue'
|
||||
import { mockSeries1 } from '@/mocks/api/handlers/series'
|
||||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { userRegular } from '@/mocks/api/handlers/users'
|
||||
import { expect } from 'storybook/test'
|
||||
import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
|
||||
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
|
||||
|
||||
const meta = {
|
||||
component: SeriesMenu,
|
||||
render: (args: object) => ({
|
||||
components: { SeriesMenu, DialogConfirmInstance, DialogConfirmEditInstance },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template:
|
||||
'<v-icon-btn id="IDce0b073e6b2146e688c1cd32b61f3fef" icon="i-mdi:dots-vertical"/><SeriesMenu v-bind="args" /><DialogConfirmInstance/><DialogConfirmEditInstance/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
activator: '#IDce0b073e6b2146e688c1cd32b61f3fef',
|
||||
series: mockSeries1,
|
||||
},
|
||||
play: async ({ canvas, userEvent }) => {
|
||||
await expect(canvas.getByRole('button')).toBeEnabled()
|
||||
|
||||
await userEvent.click(canvas.getByRole('button'))
|
||||
},
|
||||
} satisfies Meta<typeof SeriesMenu>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Read: Story = {
|
||||
args: {
|
||||
series: {
|
||||
...mockSeries1,
|
||||
booksCount: 5,
|
||||
booksReadCount: 5,
|
||||
booksUnreadCount: 0,
|
||||
booksInProgressCount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Unread: Story = {
|
||||
args: {
|
||||
series: {
|
||||
...mockSeries1,
|
||||
booksCount: 5,
|
||||
booksReadCount: 0,
|
||||
booksUnreadCount: 5,
|
||||
booksInProgressCount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const NonAdmin: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
55
next-ui/src/components/series/menu/SeriesMenu.vue
Normal file
55
next-ui/src/components/series/menu/SeriesMenu.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<v-menu :activator="activator">
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="(action, i) in actions"
|
||||
:key="i"
|
||||
v-bind="action"
|
||||
/>
|
||||
|
||||
<v-list-item
|
||||
v-if="manageActions.length > 0"
|
||||
:title="
|
||||
$formatMessage({
|
||||
description: 'Series menu: manage',
|
||||
defaultMessage: 'Manage series',
|
||||
id: 'Ougw+k',
|
||||
})
|
||||
"
|
||||
append-icon="i-mdi:menu-right"
|
||||
>
|
||||
<v-menu
|
||||
activator="parent"
|
||||
open-on-click
|
||||
open-on-hover
|
||||
location="end"
|
||||
submenu
|
||||
>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
v-for="(action, i) in manageActions"
|
||||
:key="i"
|
||||
v-bind="action"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { useSeriesActions } from '@/composables/series'
|
||||
|
||||
const { activator, series } = defineProps<{
|
||||
activator: string | Element
|
||||
series: components['schemas']['SeriesDto']
|
||||
}>()
|
||||
|
||||
const { actions, manageActions } = useSeriesActions(series)
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { Meta } from '@storybook/addon-docs/blocks';
|
||||
|
||||
import * as Stories from './SeriesMenuBottomSheet.stories.ts';
|
||||
|
||||
<Meta of={Stories} />
|
||||
|
||||
# SeriesMenuBottomSheet
|
||||
|
||||
Action menu for series for touch screens.
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SeriesMenuBottomSheet from './SeriesMenuBottomSheet.vue'
|
||||
import { mockSeries1 } from '@/mocks/api/handlers/series'
|
||||
import { httpTyped } from '@/mocks/api/httpTyped'
|
||||
import { userRegular } from '@/mocks/api/handlers/users'
|
||||
import DialogConfirmInstance from '@/components/dialog/ConfirmInstance.vue'
|
||||
import DialogConfirmEditInstance from '@/components/dialog/ConfirmEditInstance.vue'
|
||||
|
||||
const meta = {
|
||||
component: SeriesMenuBottomSheet,
|
||||
render: (args: object) => ({
|
||||
components: { SeriesMenuBottomSheet, DialogConfirmInstance, DialogConfirmEditInstance },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template:
|
||||
'<SeriesMenuBottomSheet v-bind="args" /><DialogConfirmInstance/><DialogConfirmEditInstance/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
modelValue: true,
|
||||
series: mockSeries1,
|
||||
},
|
||||
} satisfies Meta<typeof SeriesMenuBottomSheet>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
}
|
||||
|
||||
export const Read: Story = {
|
||||
args: {
|
||||
series: {
|
||||
...mockSeries1,
|
||||
booksCount: 5,
|
||||
booksReadCount: 5,
|
||||
booksUnreadCount: 0,
|
||||
booksInProgressCount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Unread: Story = {
|
||||
args: {
|
||||
series: {
|
||||
...mockSeries1,
|
||||
booksCount: 5,
|
||||
booksReadCount: 0,
|
||||
booksUnreadCount: 5,
|
||||
booksInProgressCount: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const NonAdmin: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
42
next-ui/src/components/series/menu/SeriesMenuBottomSheet.vue
Normal file
42
next-ui/src/components/series/menu/SeriesMenuBottomSheet.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<template>
|
||||
<v-bottom-sheet
|
||||
v-model="isShown"
|
||||
inset
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(action, i) in actions"
|
||||
:key="i"
|
||||
v-bind="action"
|
||||
/>
|
||||
|
||||
<v-divider v-if="manageActions.length > 0" />
|
||||
|
||||
<v-list-item
|
||||
v-for="(action, i) in manageActions"
|
||||
:key="i"
|
||||
v-bind="action"
|
||||
/>
|
||||
</v-list>
|
||||
</v-bottom-sheet>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { useSeriesActions } from '@/composables/series'
|
||||
|
||||
const isShown = defineModel<boolean>({ default: false })
|
||||
|
||||
const { series } = defineProps<{
|
||||
series: components['schemas']['SeriesDto']
|
||||
}>()
|
||||
|
||||
function afterClick() {
|
||||
isShown.value = false
|
||||
}
|
||||
const { actions, manageActions } = useSeriesActions(series, afterClick)
|
||||
</script>
|
||||
|
||||
<script lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
||||
12
next-ui/src/composables/device.ts
Normal file
12
next-ui/src/composables/device.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { useMediaQuery } from '@vueuse/core'
|
||||
|
||||
export function usePrimaryInput() {
|
||||
const hasNoHover = useMediaQuery('(hover: none)')
|
||||
const hasCoarsePointer = useMediaQuery('(pointer: coarse)')
|
||||
|
||||
const isTouchPrimary = computed(() => hasCoarsePointer.value || hasNoHover.value)
|
||||
|
||||
return {
|
||||
isTouchPrimary: isTouchPrimary,
|
||||
}
|
||||
}
|
||||
343
next-ui/src/composables/series.ts
Normal file
343
next-ui/src/composables/series.ts
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
import type { components } from '@/generated/openapi/komga'
|
||||
import EditMetadata from '@/components/series/form/EditMetadata.vue'
|
||||
import type { ErrorCause } from '@/api/komga-client'
|
||||
import { commonMessages } from '@/utils/i18n/common-messages'
|
||||
import { useDialogsStore } from '@/stores/dialogs'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useIntl } from 'vue-intl'
|
||||
import { useDisplay } from 'vuetify'
|
||||
import { useMessagesStore } from '@/stores/messages'
|
||||
import {
|
||||
useAnalyzeSeries,
|
||||
useDeleteSeries,
|
||||
useMarkSeriesRead,
|
||||
useMarkSeriesUnread,
|
||||
useRefreshMetadataSeries,
|
||||
useUpdateSeriesMetadata,
|
||||
} from '@/colada/series'
|
||||
import { useCurrentUser } from '@/colada/users'
|
||||
import { SeriesAction } from '@/types/series'
|
||||
import SeriesDeletionWarning from '@/components/series/DeletionWarning.vue'
|
||||
|
||||
export function useEditSeriesMetadataDialog() {
|
||||
const { confirmEdit: dialogConfirmEdit } = storeToRefs(useDialogsStore())
|
||||
const intl = useIntl()
|
||||
const display = useDisplay()
|
||||
const messagesStore = useMessagesStore()
|
||||
const { mutateAsync: mutateUpdateSeriesMetadata } = useUpdateSeriesMetadata()
|
||||
|
||||
const showDialog = (series: components['schemas']['SeriesDto']) => {
|
||||
dialogConfirmEdit.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Edit series metadata dialog title',
|
||||
defaultMessage: 'Edit series metadata',
|
||||
id: '1bxWGd',
|
||||
}),
|
||||
subtitle: series.metadata.title,
|
||||
maxWidth: 600,
|
||||
okText: 'Save',
|
||||
cardTextClass: 'px-0',
|
||||
closeOnSave: false,
|
||||
scrollable: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirmEdit.value.slot = {
|
||||
component: markRaw(EditMetadata),
|
||||
}
|
||||
dialogConfirmEdit.value.record = series.metadata
|
||||
dialogConfirmEdit.value.callback = (
|
||||
hideDialog: () => void,
|
||||
setLoading: (isLoading: boolean) => void,
|
||||
) => {
|
||||
setLoading(true)
|
||||
|
||||
const updatedMetadata = dialogConfirmEdit.value
|
||||
.record as components['schemas']['SeriesMetadataDto']
|
||||
|
||||
mutateUpdateSeriesMetadata({ seriesId: series.id, metadata: updatedMetadata })
|
||||
.then(() => {
|
||||
hideDialog()
|
||||
messagesStore.messages.push({
|
||||
text: intl.formatMessage(
|
||||
{
|
||||
description: 'Snackbar notification shown upon successful series metadata update',
|
||||
defaultMessage: 'Series metadata updated: {series}',
|
||||
id: 'gEBeQv',
|
||||
},
|
||||
{
|
||||
series: updatedMetadata.title,
|
||||
},
|
||||
),
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message ||
|
||||
intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const activatorRef = computed({
|
||||
get: () => dialogConfirmEdit.value.activator,
|
||||
set: (val) => (dialogConfirmEdit.value.activator = val),
|
||||
})
|
||||
|
||||
return {
|
||||
showDialog: showDialog,
|
||||
activator: activatorRef,
|
||||
}
|
||||
}
|
||||
|
||||
export function useSeriesActions(
|
||||
series: components['schemas']['SeriesDto'],
|
||||
callback: (action: SeriesAction) => void = () => {},
|
||||
) {
|
||||
const { isAdmin } = useCurrentUser()
|
||||
const intl = useIntl()
|
||||
const { confirm: dialogConfirm } = storeToRefs(useDialogsStore())
|
||||
const messagesStore = useMessagesStore()
|
||||
const display = useDisplay()
|
||||
|
||||
const actions = computed(() => [
|
||||
...(isAdmin.value
|
||||
? [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: add to collection',
|
||||
defaultMessage: 'Add to collection',
|
||||
id: 'BAKokv',
|
||||
}),
|
||||
action: SeriesAction.ADD_TO_COLLECTION,
|
||||
onClick: () => {
|
||||
todo()
|
||||
callback(SeriesAction.ADD_TO_COLLECTION)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isAdmin.value
|
||||
? [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: add to read list',
|
||||
defaultMessage: 'Add to read list',
|
||||
id: '9eEylZ',
|
||||
}),
|
||||
action: SeriesAction.ADD_TO_READLIST,
|
||||
onClick: () => {
|
||||
todo()
|
||||
callback(SeriesAction.ADD_TO_READLIST)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(series.booksReadCount === series.booksCount
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: mark as read',
|
||||
defaultMessage: 'Mark as read',
|
||||
id: 'SZWIZ7',
|
||||
}),
|
||||
action: SeriesAction.MARK_READ,
|
||||
onClick: () => {
|
||||
markRead()
|
||||
callback(SeriesAction.MARK_READ)
|
||||
},
|
||||
},
|
||||
]),
|
||||
...(series.booksUnreadCount === series.booksCount
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: mark as unread',
|
||||
defaultMessage: 'Mark as unread',
|
||||
id: 'JL33DG',
|
||||
}),
|
||||
action: SeriesAction.MARK_UNREAD,
|
||||
onClick: () => {
|
||||
markUnread()
|
||||
callback(SeriesAction.MARK_UNREAD)
|
||||
},
|
||||
},
|
||||
]),
|
||||
])
|
||||
|
||||
const manageActions = computed(() => [
|
||||
...(isAdmin.value
|
||||
? [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: manage > edit metadata',
|
||||
defaultMessage: 'Edit metadata',
|
||||
id: 'O839kY',
|
||||
}),
|
||||
action: SeriesAction.EDIT_METADATA,
|
||||
onMouseenter: (event: Event) =>
|
||||
(editMetadataActivator.value = event.currentTarget as Element),
|
||||
onClick: () => {
|
||||
updateSeriesMetadata()
|
||||
callback(SeriesAction.EDIT_METADATA)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isAdmin.value
|
||||
? [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: manage > refresh metadata',
|
||||
defaultMessage: 'Refresh metadata',
|
||||
id: 'JFtKtC',
|
||||
}),
|
||||
action: SeriesAction.REFRESH_METADATA,
|
||||
onClick: () => {
|
||||
refreshMetadata()
|
||||
callback(SeriesAction.REFRESH_METADATA)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isAdmin.value
|
||||
? [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: manage > analyze',
|
||||
defaultMessage: 'Analyze',
|
||||
id: 'AI60X8',
|
||||
}),
|
||||
action: SeriesAction.ANALYZE,
|
||||
onClick: () => {
|
||||
analyzeSeries()
|
||||
callback(SeriesAction.ANALYZE)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(isAdmin.value
|
||||
? [
|
||||
{
|
||||
title: intl.formatMessage({
|
||||
description: 'Series menu: manage > delete file',
|
||||
defaultMessage: 'Delete files',
|
||||
id: 'J+H9cC',
|
||||
}),
|
||||
action: SeriesAction.DELETE_FILES,
|
||||
onMouseenter: (event: Event) =>
|
||||
(dialogConfirm.value.activator = event.currentTarget as Element),
|
||||
onClick: () => {
|
||||
deleteSeries()
|
||||
callback(SeriesAction.DELETE_FILES)
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
])
|
||||
|
||||
//region Update Series metadata
|
||||
const { showDialog: showEditSeriesMetadataDialog, activator: editMetadataActivator } =
|
||||
useEditSeriesMetadataDialog()
|
||||
|
||||
function updateSeriesMetadata() {
|
||||
showEditSeriesMetadataDialog(series)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Refresh Metadata
|
||||
const { mutate: mutateRefreshMetadata } = useRefreshMetadataSeries()
|
||||
|
||||
function refreshMetadata() {
|
||||
mutateRefreshMetadata(series.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Analyze
|
||||
const { mutate: mutateAnalyze } = useAnalyzeSeries()
|
||||
|
||||
function analyzeSeries() {
|
||||
mutateAnalyze(series.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
function todo() {}
|
||||
|
||||
//region Mark read
|
||||
const { mutate: mutateMarkRead } = useMarkSeriesRead()
|
||||
|
||||
function markRead() {
|
||||
mutateMarkRead(series.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Mark unread
|
||||
const { mutate: mutateMarkUnread } = useMarkSeriesUnread()
|
||||
|
||||
function markUnread() {
|
||||
mutateMarkUnread(series.id)
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region Delete
|
||||
const { mutateAsync: mutateDelete } = useDeleteSeries()
|
||||
|
||||
function deleteSeries() {
|
||||
dialogConfirm.value.dialogProps = {
|
||||
title: intl.formatMessage({
|
||||
description: 'Series delete dialog: title',
|
||||
defaultMessage: 'Delete series files',
|
||||
id: 'Xxu514',
|
||||
}),
|
||||
subtitle: series.metadata.title,
|
||||
maxWidth: 600,
|
||||
mode: 'checkbox',
|
||||
color: 'error',
|
||||
okText: intl.formatMessage({
|
||||
description: 'Series delete dialog: confirm button',
|
||||
defaultMessage: 'Delete files',
|
||||
id: 'DfakWW',
|
||||
}),
|
||||
closeOnSave: true,
|
||||
fullscreen: display.xs.value,
|
||||
}
|
||||
dialogConfirm.value.slotWarning = {
|
||||
component: markRaw(SeriesDeletionWarning),
|
||||
props: {},
|
||||
}
|
||||
dialogConfirm.value.callback = () => {
|
||||
mutateDelete(series.id)
|
||||
.then(() => {
|
||||
messagesStore.messages.push({
|
||||
text: intl.formatMessage(
|
||||
{
|
||||
description: 'Snackbar notification shown upon successful series files deletion',
|
||||
defaultMessage: 'Series files deleted: {series}',
|
||||
id: 'aSDxrt',
|
||||
},
|
||||
{
|
||||
series: series.metadata.title,
|
||||
},
|
||||
),
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
messagesStore.messages.push({
|
||||
text:
|
||||
(error?.cause as ErrorCause)?.message ||
|
||||
intl.formatMessage(commonMessages.networkError),
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
||||
return {
|
||||
actions: actions,
|
||||
manageActions: manageActions,
|
||||
}
|
||||
}
|
||||
14
next-ui/src/functions/series.ts
Normal file
14
next-ui/src/functions/series.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import type { components } from '@/generated/openapi/komga'
|
||||
|
||||
export function seriesMetadataToDto(
|
||||
metadata: components['schemas']['SeriesMetadataDto'],
|
||||
): components['schemas']['SeriesMetadataUpdateDto'] {
|
||||
return Object.assign({}, metadata, {
|
||||
readingDirection: metadata.readingDirection as
|
||||
| 'LEFT_TO_RIGHT'
|
||||
| 'RIGHT_TO_LEFT'
|
||||
| 'VERTICAL'
|
||||
| 'WEBTOON',
|
||||
status: metadata.status as 'ENDED' | 'ONGOING' | 'ABANDONED' | 'HIATUS',
|
||||
})
|
||||
}
|
||||
|
|
@ -150,4 +150,16 @@ export const seriesHandlers = [
|
|||
},
|
||||
})
|
||||
}),
|
||||
httpTyped.patch('/api/v1/series/{seriesId}/metadata', ({ response }) => response(204).empty()),
|
||||
httpTyped.post('/api/v1/series/{seriesId}/analyze', ({ response }) => response(202).empty()),
|
||||
httpTyped.post('/api/v1/series/{seriesId}/metadata/refresh', ({ response }) =>
|
||||
response(202).empty(),
|
||||
),
|
||||
httpTyped.delete('/api/v1/series/{seriesId}/file', ({ response }) => response(202).empty()),
|
||||
httpTyped.post('/api/v1/series/{seriesId}/read-progress', ({ response }) =>
|
||||
response(204).empty(),
|
||||
),
|
||||
httpTyped.delete('/api/v1/series/{seriesId}/read-progress', ({ response }) =>
|
||||
response(204).empty(),
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -14,3 +14,14 @@ html {
|
|||
.link-underline:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
// This requires the following CSS variables to be set:
|
||||
// --lines: the number of lines to display
|
||||
// --line-height: the line height
|
||||
.force-line-count {
|
||||
display: -webkit-box !important;
|
||||
-webkit-line-clamp: var(--lines, 2);
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden !important;
|
||||
height: calc(var(--line-height) * var(--lines, 1) * 1rem);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,33 @@ export type ItemCardProps = {
|
|||
export type ItemCardEmits = {
|
||||
selection: [selected: boolean]
|
||||
}
|
||||
|
||||
export type ItemCardTitle = {
|
||||
/**
|
||||
* Text to display.
|
||||
*/
|
||||
text: string
|
||||
/**
|
||||
* Number of lines.
|
||||
*/
|
||||
lines?: number
|
||||
}
|
||||
|
||||
export type ItemCardLine = {
|
||||
/**
|
||||
* Text to display.
|
||||
*/
|
||||
text?: string
|
||||
/**
|
||||
* Classes to apply.
|
||||
*/
|
||||
classes?: string
|
||||
/**
|
||||
* Number of lines of text.
|
||||
*/
|
||||
lines?: number
|
||||
/**
|
||||
* Whether the container will be shown even if `text` is empty.
|
||||
*/
|
||||
allowEmpty?: boolean
|
||||
}
|
||||
|
|
|
|||
10
next-ui/src/types/series.ts
Normal file
10
next-ui/src/types/series.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
export enum SeriesAction {
|
||||
ADD_TO_COLLECTION,
|
||||
ADD_TO_READLIST,
|
||||
MARK_READ,
|
||||
MARK_UNREAD,
|
||||
EDIT_METADATA,
|
||||
REFRESH_METADATA,
|
||||
ANALYZE,
|
||||
DELETE_FILES,
|
||||
}
|
||||
Loading…
Reference in a new issue