Skip to content
Draft
30 changes: 30 additions & 0 deletions packages/client/composables/useDynamicVirtualList.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { UseVirtualListOptions } from '@vueuse/core'
import { debouncedWatch, useVirtualList } from '@vueuse/core'
import type { MaybeRef } from 'vue'
import { effectScope, shallowRef } from 'vue'

/**
* `useVirtualList`'s `itemHeight` is not reactive, so we need to re-create the virtual list when the card height changes.
*/
export function useDynamicVirtualList<T>(list: MaybeRef<T[]>, getOptions: () => UseVirtualListOptions) {
type VirtualListReturn = ReturnType<typeof useVirtualList<T>>
const virtualList = shallowRef<VirtualListReturn>()
debouncedWatch(
getOptions,
(options, _oldOptions, onCleanup) => {
const scope = effectScope()
scope.run(() => {
virtualList.value = useVirtualList(
list,
options,
)
})
onCleanup(() => scope.stop())
},
{
immediate: true,
debounce: 50,
},
)
return virtualList
}
122 changes: 68 additions & 54 deletions packages/client/internals/QuickOverview.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
import { useElementSize, useEventListener } from '@vueuse/core'
import { computed, ref, watchEffect } from 'vue'
import { breakpoints, showOverview, windowSize } from '../state'
import { breakpoints, showOverview } from '../state'
import { currentOverviewPage, overviewRowCount } from '../logic/overview'
import { createFixedClicks } from '../composables/useClicks'
import { CLICKS_MAX } from '../constants'
import { useNav } from '../composables/useNav'
import { pathPrefix } from '../env'
import { pathPrefix, slideAspect } from '../env'
import { useDynamicVirtualList } from '../composables/useDynamicVirtualList'
import SlideContainer from './SlideContainer.vue'
import SlideWrapper from './SlideWrapper.vue'
import DrawingPreview from './DrawingPreview.vue'
Expand All @@ -23,29 +24,39 @@ function go(page: number) {
close()
}

function focus(page: number) {
if (page === currentOverviewPage.value)
return true
return false
}

const xs = breakpoints.smaller('xs')
const sm = breakpoints.smaller('sm')

const padding = 4 * 16 * 2
const gap = 2 * 16
const gapX = 2 * 16
const gapY = 4 * 8 // mb-8

const containerEl = ref<HTMLElement>()
const { width: containerWidth } = useElementSize(containerEl)

const cardWidth = computed(() => {
if (xs.value)
return windowSize.width.value - padding
else if (sm.value)
return (windowSize.width.value - padding - gap) / 2
return 300
return xs.value
? containerWidth.value
: Math.min(300, (containerWidth.value - gapX) / 2)
})

const rowCount = computed(() => {
return Math.floor((windowSize.width.value - padding) / (cardWidth.value + gap))
const numOfCols = computed(() => {
return xs.value
? 1
: Math.floor((containerWidth.value + gapX) / (cardWidth.value + gapX))
})

const cardHeight = computed(() => cardWidth.value / slideAspect.value)

const virtualList = useDynamicVirtualList(slides, () => ({
itemHeight: (i) => {
if (i === slides.value.length - 1)
return cardHeight.value + 2
if (i % numOfCols.value === numOfCols.value - 1)
return cardHeight.value + gapY + 2
return 0
},
overscan: 4 * numOfCols.value,
}))

const keyboardBuffer = ref<string>('')

useEventListener('keypress', (e) => {
Expand Down Expand Up @@ -96,7 +107,7 @@ watchEffect(() => {
// we focus on the right page.
currentOverviewPage.value = currentSlideNo.value
// Watch rowCount, make sure up and down shortcut work correctly.
overviewRowCount.value = rowCount.value
overviewRowCount.value = numOfCols.value
})
</script>

Expand All @@ -109,47 +120,50 @@ watchEffect(() => {
>
<div
v-if="showOverview"
class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-20 bg-main !bg-opacity-75 p-16 py-20 overflow-y-auto backdrop-blur-5px"
v-bind="virtualList?.containerProps"
class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-20 bg-main !bg-opacity-75 px-16 py-20 overflow-y-auto backdrop-blur-5px"
@click="close"
>
<div
class="grid gap-y-4 gap-x-8 w-full"
:style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
>
<div ref="containerEl" v-bind="virtualList?.wrapperProps.value">
<div
v-for="(route, idx) of slides"
:key="route.no"
class="relative"
class="grid w-full"
:style="`grid-template-columns: repeat(${numOfCols},minmax(${cardWidth}px,1fr)); grid-auto-rows: ${cardHeight + 2}px; gap: ${gapX}px ${gapY}px`"
>
<div
class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition"
:class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-primary' : 'border-main'"
@click="go(route.no)"
v-for="{ data: route } of virtualList?.list.value"
:key="route.no"
class="relative"
>
<SlideContainer
:key="route.no"
:width="cardWidth"
class="pointer-events-none"
<div
class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition"
:class="currentOverviewPage === route.no ? 'border-primary' : 'border-main'"
@click="go(route.no)"
>
<SlideWrapper
:clicks-context="createFixedClicks(route, CLICKS_MAX)"
:route="route"
render-context="overview"
/>
<DrawingPreview :page="route.no" />
</SlideContainer>
</div>
<div
class="absolute top-0"
:style="`left: ${cardWidth + 5}px`"
>
<template v-if="keyboardBuffer && String(idx + 1).startsWith(keyboardBuffer)">
<span class="text-green font-bold">{{ keyboardBuffer }}</span>
<span class="opacity-50">{{ String(idx + 1).slice(keyboardBuffer.length) }}</span>
</template>
<span v-else class="opacity-50">
{{ idx + 1 }}
</span>
<SlideContainer
:key="route.no"
:width="cardWidth"
class="pointer-events-none"
>
<SlideWrapper
:clicks-context="createFixedClicks(route, CLICKS_MAX)"
:route="route"
render-context="overview"
/>
<DrawingPreview :page="route.no" />
</SlideContainer>
</div>
<div
class="absolute top-0"
:style="`left: ${cardWidth + 5}px`"
>
<template v-if="keyboardBuffer && String(route.no).startsWith(keyboardBuffer)">
<span class="text-green font-bold">{{ keyboardBuffer }}</span>
<span class="opacity-50">{{ String(route.no).slice(keyboardBuffer.length) }}</span>
</template>
<span v-else class="opacity-50">
{{ route.no }}
</span>
</div>
</div>
</div>
</div>
Expand Down
Loading