mirror of
https://github.com/gotson/komga.git
synced 2026-05-08 04:22:28 +02:00
basic card wide
This commit is contained in:
parent
3789f07eff
commit
368c390e2f
7 changed files with 595 additions and 11 deletions
2
next-ui/src/components.d.ts
vendored
2
next-ui/src/components.d.ts
vendored
|
|
@ -49,6 +49,7 @@ declare module 'vue' {
|
|||
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']
|
||||
ItemCardWide: typeof import('./components/item/CardWide/ItemCardWide.vue')['default']
|
||||
LayoutAppBar: typeof import('./components/layout/app/Bar.vue')['default']
|
||||
LayoutAppBarHolder: typeof import('./components/layout/app/BarHolder.vue')['default']
|
||||
LayoutAppDrawer: typeof import('./components/layout/app/drawer/Drawer.vue')['default']
|
||||
|
|
@ -86,6 +87,7 @@ declare module 'vue' {
|
|||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SelectionBar: typeof import('./components/selection/Bar.vue')['default']
|
||||
SeriesCard: typeof import('./components/series/card/SeriesCard.vue')['default']
|
||||
SeriesCardWide: typeof import('./components/series/CardWide/SeriesCardWide.vue')['default']
|
||||
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']
|
||||
|
|
|
|||
166
next-ui/src/components/item/CardWide/ItemCardWide.stories.ts
Normal file
166
next-ui/src/components/item/CardWide/ItemCardWide.stories.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import ItemCardWide from './ItemCardWide.vue'
|
||||
import { seriesThumbnailUrl } from '@/api/images'
|
||||
import { delay, http } from 'msw'
|
||||
import { fn } from 'storybook/test'
|
||||
|
||||
const meta = {
|
||||
component: ItemCardWide,
|
||||
render: (args: object) => ({
|
||||
components: { ItemCardWide },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template: '<ItemCardWide v-bind="args" />',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: 'A flexible card that serves as the base for entity cards.',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
title: 'Card title',
|
||||
text: 'Card content',
|
||||
posterUrl: seriesThumbnailUrl('id'),
|
||||
width: 150,
|
||||
onSelection: fn(),
|
||||
onClickFab: fn(),
|
||||
onClickQuickAction: fn(),
|
||||
onClickMenu: fn(),
|
||||
preSelect: false,
|
||||
selected: false,
|
||||
stretchPoster: true,
|
||||
menuIcon: 'i-mdi:menu',
|
||||
quickActionIcon: 'i-mdi:pencil',
|
||||
},
|
||||
argTypes: {
|
||||
posterUrl: {
|
||||
options: [seriesThumbnailUrl('id'), seriesThumbnailUrl('idL')],
|
||||
control: { type: 'radio' },
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof ItemCardWide>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
stretchPoster: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const LongText: Story = {
|
||||
args: {
|
||||
title:
|
||||
'The Fractured World of Miravara this is a super long title that should overshoot the container, but that should still be alright because it should wrap nicely',
|
||||
text: `The story unfolds on Miravara, a planet encased within a colossal, translucent barrier known as the Glass Frontier. For centuries, this barrier has protected the populace from a dying sun, filtering its radiation into usable power. Entire cities cling to its surface like glowing barnacles—each one a hybrid of sleek crystal towers and decaying steel foundations. Below the barrier lies the Underglow, a vast labyrinth of machinery and forgotten people who maintain the world’s vital energy grid, unseen by those above.
|
||||
|
||||
Politically, Miravara is divided between the High Dominion—wealthy citizens who live near the surface of the barrier—and the Conduits, a laboring caste living in the Underglow. Above them all looms the enigmatic Council of Luminarchs, a secretive group rumored to communicate directly with the Glass Frontier itself.`,
|
||||
stretchPoster: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const TopRightCount: Story = {
|
||||
args: {
|
||||
topRight: 24,
|
||||
},
|
||||
}
|
||||
|
||||
export const TopRightIcon: Story = {
|
||||
args: {
|
||||
topRightIcon: 'i-mdi:check',
|
||||
},
|
||||
}
|
||||
|
||||
export const LandscapeStretched: Story = {
|
||||
args: {
|
||||
topRightIcon: 'i-mdi:check',
|
||||
posterUrl: seriesThumbnailUrl('idL'),
|
||||
stretchPoster: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const LandscapeNotStretched: Story = {
|
||||
args: {
|
||||
topRightIcon: 'i-mdi:check',
|
||||
posterUrl: seriesThumbnailUrl('idL'),
|
||||
stretchPoster: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const QuickActionIcon: Story = {
|
||||
args: {
|
||||
quickActionIcon: 'i-mdi:pencil',
|
||||
},
|
||||
play: ({ canvas, userEvent }) => {
|
||||
userEvent.hover(canvas.getByRole('img'))
|
||||
},
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
userEvent.hover(canvas.getByRole('img'))
|
||||
},
|
||||
}
|
||||
|
||||
export const DisableSelection: Story = {
|
||||
args: {
|
||||
disableSelection: true,
|
||||
},
|
||||
play: ({ canvas, userEvent }) => {
|
||||
userEvent.hover(canvas.getByRole('img'))
|
||||
},
|
||||
}
|
||||
|
||||
export const Selected: Story = {
|
||||
args: {
|
||||
selected: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const PreSelect: Story = {
|
||||
args: {
|
||||
preSelect: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Progress: Story = {
|
||||
args: {
|
||||
progressPercent: 33,
|
||||
},
|
||||
}
|
||||
|
||||
export const Big: Story = {
|
||||
args: {
|
||||
width: 300,
|
||||
},
|
||||
}
|
||||
|
||||
export const PosterError: Story = {
|
||||
args: {
|
||||
posterUrl: '/error',
|
||||
},
|
||||
}
|
||||
|
||||
export const Loading: Story = {
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [http.all('*/api/*', async () => await delay(5_000))],
|
||||
},
|
||||
},
|
||||
}
|
||||
235
next-ui/src/components/item/CardWide/ItemCardWide.vue
Normal file
235
next-ui/src/components/item/CardWide/ItemCardWide.vue
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
<template>
|
||||
<v-hover
|
||||
v-slot="{ props }"
|
||||
v-model="isHovering"
|
||||
>
|
||||
<v-card
|
||||
v-on-long-press.prevent="onCardLongPress"
|
||||
v-bind="props"
|
||||
:elevation="isHovering ? 3 : 1"
|
||||
:class="isPreSelect || selected ? 'cursor-pointer' : 'cursor-default'"
|
||||
@click="isPreSelect || selected ? emit('selection', !selected) : {}"
|
||||
>
|
||||
<v-card-text>
|
||||
<div class="d-flex flex-row ga-4">
|
||||
<div>
|
||||
<v-fade-transition>
|
||||
<v-icon-btn
|
||||
v-if="showSelection"
|
||||
:icon="
|
||||
selected || (isPreSelect && isHovering)
|
||||
? 'i-mdi:checkbox-marked-circle'
|
||||
: 'i-mdi:checkbox-blank-circle-outline'
|
||||
"
|
||||
:color="selected ? 'primary' : undefined"
|
||||
:variant="selected ? 'text' : 'flat'"
|
||||
class="position-absolute top-0 left-0"
|
||||
style="z-index: 2"
|
||||
@click.stop="emit('selection', !selected)"
|
||||
/>
|
||||
</v-fade-transition>
|
||||
|
||||
<v-img
|
||||
:cover="stretchPoster"
|
||||
:position="stretchPoster ? 'top ' : undefined"
|
||||
:src="posterUrl"
|
||||
lazy-src="@/assets/cover.svg"
|
||||
aspect-ratio="0.7071"
|
||||
:class="{ normal: true, inset: selected }"
|
||||
:width="width"
|
||||
>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height">
|
||||
<v-progress-circular
|
||||
color="grey"
|
||||
indeterminate
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- This will just show lazy-src without the v-progress -->
|
||||
<template #error></template>
|
||||
|
||||
<!-- Top-right icon -->
|
||||
<div
|
||||
v-if="topRightIcon || topRight"
|
||||
class="top-0 right-0 position-absolute translucent text-white px-2 py-1 font-weight-bold text-caption"
|
||||
style="border-bottom-left-radius: 4px"
|
||||
>
|
||||
<v-icon
|
||||
v-if="topRightIcon"
|
||||
:icon="topRightIcon"
|
||||
/>
|
||||
<template v-else>{{ topRight }}</template>
|
||||
</div>
|
||||
|
||||
<!-- Progress bar -->
|
||||
<v-progress-linear
|
||||
v-if="progressPercent"
|
||||
:model-value="progressPercent"
|
||||
color="primary"
|
||||
height="10"
|
||||
class="position-absolute bottom-0"
|
||||
style="top: unset"
|
||||
/>
|
||||
</v-img>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="text-h6 force-line-count text-wrap"
|
||||
:style="[{ '--lines': 1 }, { '--line-height': 1.6 }]"
|
||||
>
|
||||
{{ title }}
|
||||
</div>
|
||||
<div
|
||||
class="text-subtitle-1 force-line-count text-pre-wrap mt-2"
|
||||
:style="[{ '--lines': display.xs.value ? 1 : 3 }, { '--line-height': 1.6 }]"
|
||||
>
|
||||
{{ text }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- icon holder -->
|
||||
<div class="d-flex flex-column ga-1 position-absolute top-0 right-0 mt-2 me-2">
|
||||
<!-- menu icon -->
|
||||
<v-fade-transition>
|
||||
<v-icon-btn
|
||||
v-if="menuIcon && showMenu"
|
||||
:icon="menuIcon"
|
||||
v-bind="menuProps"
|
||||
@click.stop="emit('clickMenu')"
|
||||
/>
|
||||
</v-fade-transition>
|
||||
|
||||
<!-- quick action icon -->
|
||||
<v-fade-transition>
|
||||
<v-icon-btn
|
||||
v-if="quickActionIcon && showQuickAction"
|
||||
:icon="quickActionIcon"
|
||||
v-bind="quickActionProps"
|
||||
@click.stop="emit('clickQuickAction')"
|
||||
/>
|
||||
</v-fade-transition>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ItemCardEmits, ItemCardProps } from '@/types/ItemCard'
|
||||
import { vOnLongPress } from '@vueuse/components'
|
||||
import { usePrimaryInput } from '@/composables/device'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const display = useDisplay()
|
||||
const { isTouchPrimary } = usePrimaryInput()
|
||||
|
||||
function onCardLongPress() {
|
||||
if (isTouchPrimary.value) emit('cardLongPress')
|
||||
}
|
||||
|
||||
const {
|
||||
stretchPoster,
|
||||
width = 150,
|
||||
disableSelection = false,
|
||||
selected = false,
|
||||
preSelect = false,
|
||||
quickActionIcon,
|
||||
quickActionProps = {},
|
||||
menuIcon,
|
||||
} = defineProps<
|
||||
ItemCardProps & {
|
||||
/**
|
||||
* Poster URL.
|
||||
*/
|
||||
posterUrl?: string
|
||||
/**
|
||||
* Card title.
|
||||
*/
|
||||
title?: string
|
||||
/**
|
||||
* Text to display under the title.
|
||||
*/
|
||||
text?: string
|
||||
/**
|
||||
* Number displayed in the top-right corner.
|
||||
*/
|
||||
topRight?: number
|
||||
/**
|
||||
* Icon displayed in the top-right corner. Takes precedence over `topRight`.
|
||||
*/
|
||||
topRightIcon?: string
|
||||
/**
|
||||
* Icon displayed in the bottom-left corner.
|
||||
*/
|
||||
quickActionIcon?: string
|
||||
/**
|
||||
* Props to pass to the menu icon element.
|
||||
*/
|
||||
quickActionProps?: Record<string, unknown>
|
||||
/**
|
||||
* Icon displayed in the bottom-right corner.
|
||||
*/
|
||||
menuIcon?: string
|
||||
/**
|
||||
* Props to pass to the menu icon element.
|
||||
*/
|
||||
menuProps?: object
|
||||
progressPercent?: number
|
||||
}
|
||||
>()
|
||||
|
||||
const emit = defineEmits<
|
||||
ItemCardEmits & {
|
||||
clickFab: []
|
||||
clickQuickAction: []
|
||||
clickMenu: []
|
||||
cardLongPress: []
|
||||
}
|
||||
>()
|
||||
|
||||
const isHovering = ref(false)
|
||||
|
||||
const isPreSelect = computed(() => preSelect && !selected && !disableSelection)
|
||||
const showSelection = computed(
|
||||
() => !disableSelection && (isHovering.value || isPreSelect.value || selected),
|
||||
)
|
||||
const showQuickAction = computed(
|
||||
() => (isHovering.value || isTouchPrimary.value) && !selected && !preSelect,
|
||||
)
|
||||
const showMenu = computed(
|
||||
() => isHovering.value && !selected && !preSelect && !isTouchPrimary.value,
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.min-height {
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.translucent {
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
// Required so that opacity change in VOverlay has transition
|
||||
.v-overlay__scrim {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
// Required as initial state so that transition happens on deselect
|
||||
.normal {
|
||||
transform: scale(1);
|
||||
transition: transform 0.135s cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
.inset {
|
||||
transform: scale(0.78);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.plain {
|
||||
opacity: 0.62;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -65,7 +65,7 @@
|
|||
transition="fade-transition"
|
||||
content-class="fill-height w-100"
|
||||
>
|
||||
<!-- Top right number / icon -->
|
||||
<!-- Top left selection -->
|
||||
<v-icon-btn
|
||||
v-if="!hideSelection"
|
||||
:icon="
|
||||
|
|
|
|||
|
|
@ -0,0 +1,96 @@
|
|||
import type { Meta, StoryObj } from '@storybook/vue3-vite'
|
||||
|
||||
import SeriesCardWide from './SeriesCardWide.vue'
|
||||
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: SeriesCardWide,
|
||||
render: (args: object) => ({
|
||||
components: { SeriesCardWide, DialogConfirmEditInstance, DialogConfirmInstance },
|
||||
setup() {
|
||||
return { args }
|
||||
},
|
||||
template:
|
||||
'<SeriesCardWide v-bind="args" /><DialogConfirmEditInstance/><DialogConfirmInstance/>',
|
||||
}),
|
||||
parameters: {
|
||||
// More on how to position stories at: https://storybook.js.org/docs/configure/story-layout
|
||||
docs: {
|
||||
description: {
|
||||
component: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
args: {
|
||||
series: mockSeries1,
|
||||
onSelection: fn(),
|
||||
},
|
||||
} satisfies Meta<typeof SeriesCardWide>
|
||||
|
||||
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 Oneshot: Story = {
|
||||
args: {
|
||||
series: {
|
||||
...mockSeries1,
|
||||
oneshot: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Deleted: Story = {
|
||||
args: {
|
||||
series: {
|
||||
...mockSeries1,
|
||||
deleted: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const Selected: Story = {
|
||||
args: {
|
||||
selected: true,
|
||||
},
|
||||
}
|
||||
|
||||
export const Hover: Story = {
|
||||
args: {},
|
||||
play: ({ canvas, userEvent }) => {
|
||||
userEvent.hover(canvas.getByRole('img'))
|
||||
},
|
||||
}
|
||||
|
||||
export const HoverNonAdmin: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
msw: {
|
||||
handlers: [
|
||||
httpTyped.get('/api/v2/users/me', ({ response }) => response(200).json(userRegular)),
|
||||
],
|
||||
},
|
||||
},
|
||||
play: ({ canvas, userEvent }) => {
|
||||
userEvent.hover(canvas.getByRole('img'))
|
||||
},
|
||||
}
|
||||
67
next-ui/src/components/series/CardWide/SeriesCardWide.vue
Normal file
67
next-ui/src/components/series/CardWide/SeriesCardWide.vue
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<ItemCardWide
|
||||
:title="series.metadata.title"
|
||||
:text="series.metadata.summary"
|
||||
:poster-url="seriesThumbnailUrl(series.id)"
|
||||
:top-right="unreadCount"
|
||||
:top-right-icon="isRead ? 'i-mdi:check' : undefined"
|
||||
:quick-action-icon="quickActionIcon"
|
||||
:quick-action-props="quickActionProps"
|
||||
:menu-icon="menuIcon"
|
||||
:menu-props="menuProps"
|
||||
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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { components } from '@/generated/openapi/komga'
|
||||
import { seriesThumbnailUrl } from '@/api/images'
|
||||
import type { ItemCardEmits, ItemCardProps } from '@/types/ItemCard'
|
||||
import { useCurrentUser } from '@/colada/users'
|
||||
|
||||
import { useEditSeriesMetadataDialog } from '@/composables/series/useEditSeriesMetadataDialog'
|
||||
|
||||
const { series, ...props } = defineProps<
|
||||
{
|
||||
series: components['schemas']['SeriesDto']
|
||||
} & ItemCardProps
|
||||
>()
|
||||
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,
|
||||
)
|
||||
|
||||
const { isAdmin } = useCurrentUser()
|
||||
const quickActionIcon = computed(() => (isAdmin.value ? 'i-mdi:pencil' : undefined))
|
||||
const quickActionProps = computed(() => ({
|
||||
onMouseenter: (event: Event) => (editMetadataActivator.value = event.currentTarget as Element),
|
||||
}))
|
||||
const menuIcon = computed(() => (isAdmin.value ? 'i-mdi:dots-vertical' : undefined))
|
||||
const menuProps = computed(() => ({
|
||||
onmouseenter: (event: Event) => (menuActivator.value = event.currentTarget as Element),
|
||||
}))
|
||||
|
||||
const { prepareDialog: showEditSeriesMetadataDialog, activator: editMetadataActivator } =
|
||||
useEditSeriesMetadataDialog()
|
||||
|
||||
function showEditMetadataDialog() {
|
||||
showEditSeriesMetadataDialog(series)
|
||||
}
|
||||
|
||||
const menuActivator = ref()
|
||||
</script>
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
<v-spacer />
|
||||
|
||||
<v-slider
|
||||
v-if="presentationMode === 'grid'"
|
||||
v-if="display.smAndUp.value"
|
||||
v-model="appStore.gridCardWidth"
|
||||
:min="130"
|
||||
:max="200"
|
||||
|
|
@ -14,8 +14,10 @@
|
|||
/>
|
||||
|
||||
<PresentationSelector
|
||||
v-if="display.smAndUp.value"
|
||||
v-model="presentationMode"
|
||||
:modes="['grid', 'list']"
|
||||
toggle
|
||||
/>
|
||||
|
||||
<PageSizeSelector
|
||||
|
|
@ -36,7 +38,7 @@
|
|||
>
|
||||
<template #default="{ items, toggleSelect, isSelected }">
|
||||
<v-container
|
||||
v-if="presentationMode === 'grid'"
|
||||
v-if="presentationModeEffective === 'grid'"
|
||||
fluid
|
||||
>
|
||||
<v-row>
|
||||
|
|
@ -50,22 +52,33 @@
|
|||
:series="item.raw"
|
||||
:selected="isSelected(item)"
|
||||
:pre-select="preSelect"
|
||||
:width="appStore.gridCardWidth"
|
||||
:width="display.xs.value ? undefined : appStore.gridCardWidth"
|
||||
@selection="toggleSelect(item)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-list v-if="presentationMode === 'list'">
|
||||
<v-list-item
|
||||
<v-container
|
||||
v-if="presentationModeEffective === 'list'"
|
||||
fluid
|
||||
>
|
||||
<v-row
|
||||
v-for="item in items"
|
||||
:key="item.raw.id"
|
||||
:title="item.raw.metadata.title"
|
||||
:base-color="isSelected(item) ? 'red' : 'blue'"
|
||||
@click="toggleSelect(item)"
|
||||
/>
|
||||
</v-list>
|
||||
>
|
||||
<v-col>
|
||||
<SeriesCardWide
|
||||
stretch-poster
|
||||
:series="item.raw"
|
||||
:selected="isSelected(item)"
|
||||
:pre-select="preSelect"
|
||||
:width="appStore.gridCardWidth"
|
||||
@selection="toggleSelect(item)"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
</v-data-iterator>
|
||||
|
||||
|
|
@ -87,15 +100,20 @@ import { useItemsPerPage, usePagination } from '@/composables/pagination'
|
|||
import { useSearchConditionLibraries } from '@/composables/search'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useSelectionStore } from '@/stores/selection'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
const route = useRoute('/libraries/[id]/series')
|
||||
const libraryId = route.params.id
|
||||
const { libraries } = useGetLibrariesById(libraryId)
|
||||
const { librariesCondition } = useSearchConditionLibraries(libraries)
|
||||
|
||||
const display = useDisplay()
|
||||
const appStore = useAppStore()
|
||||
const { browsingPageSize } = storeToRefs(appStore)
|
||||
const presentationMode = appStore.getPresentationMode(`${libraryId}_series`, 'grid')
|
||||
const presentationModeEffective = computed(() =>
|
||||
display.xs.value ? 'grid' : presentationMode.value,
|
||||
)
|
||||
|
||||
const { itemsPerPage } = useItemsPerPage(browsingPageSize)
|
||||
const { page0, page1, pageCount } = usePagination()
|
||||
|
|
|
|||
Loading…
Reference in a new issue