返回 slidev
QuickOverview.vue
根目录 / packages / client / internals / QuickOverview.vue
1 <script setup lang="ts">
2 import { useEventListener } from '@vueuse/core'
3 import { computed, ref, watchEffect } from 'vue'
4 import { createFixedClicks } from '../composables/useClicks'
5 import { useNav } from '../composables/useNav'
6 import { CLICKS_MAX } from '../constants'
7 import { pathPrefix } from '../env'
8 import { currentOverviewPage, overviewRowCount } from '../logic/overview'
9 import { isScreenshotSupported } from '../logic/screenshot'
10 import { snapshotManager } from '../logic/snapshot'
11 import { breakpoints, showOverview, windowSize } from '../state'
12 import DrawingPreview from './DrawingPreview.vue'
13 import IconButton from './IconButton.vue'
14 import SlideContainer from './SlideContainer.vue'
15 import SlideWrapper from './SlideWrapper.vue'
16
17 const nav = useNav()
18 const { currentSlideNo, go: goSlide, slides } = nav
19
20 function close() {
21 showOverview.value = false
22 }
23
24 function go(page: number) {
25 goSlide(page)
26 close()
27 }
28
29 function focus(page: number) {
30 if (page === currentOverviewPage.value)
31 return true
32 return false
33 }
34
35 const xs = breakpoints.smaller('xs')
36 const sm = breakpoints.smaller('sm')
37
38 const padding = 4 * 16 * 2
39 const gap = 2 * 16
40 const cardWidth = computed(() => {
41 if (xs.value)
42 return windowSize.width.value - padding
43 else if (sm.value)
44 return (windowSize.width.value - padding - gap) / 2
45 return 300
46 })
47
48 const rowCount = computed(() => {
49 return Math.floor((windowSize.width.value - padding) / (cardWidth.value + gap))
50 })
51
52 const keyboardBuffer = ref<string>('')
53
54 async function captureSlidesOverview() {
55 showOverview.value = false
56 await snapshotManager.startCapturing(nav)
57 showOverview.value = true
58 }
59
60 useEventListener('keypress', (e) => {
61 if (!showOverview.value) {
62 keyboardBuffer.value = ''
63 return
64 }
65 if (e.key === 'Enter') {
66 e.preventDefault()
67 if (keyboardBuffer.value) {
68 go(+keyboardBuffer.value)
69 keyboardBuffer.value = ''
70 }
71 else {
72 go(currentOverviewPage.value)
73 }
74 return
75 }
76 const num = Number.parseInt(e.key.replace(/\D/g, ''))
77 if (Number.isNaN(num)) {
78 keyboardBuffer.value = ''
79 return
80 }
81 if (!keyboardBuffer.value && num === 0)
82 return
83
84 keyboardBuffer.value += String(num)
85
86 // beyond the number of slides, reset
87 if (+keyboardBuffer.value > slides.value.length) {
88 keyboardBuffer.value = ''
89 return
90 }
91
92 const extactMatch = slides.value.findIndex(i => `/${i.no}` === keyboardBuffer.value)
93 if (extactMatch !== -1)
94 currentOverviewPage.value = extactMatch + 1
95
96 // When the input number is the largest at the number of digits, we go to that page directly.
97 if (+keyboardBuffer.value * 10 > slides.value.length) {
98 go(+keyboardBuffer.value)
99 keyboardBuffer.value = ''
100 }
101 })
102
103 watchEffect(() => {
104 // Watch currentPage, make sure every time we open overview,
105 // we focus on the right page.
106 currentOverviewPage.value = currentSlideNo.value
107 // Watch rowCount, make sure up and down shortcut work correctly.
108 overviewRowCount.value = rowCount.value
109 })
110 </script>
111
112 <template>
113 <Transition
114 enter-active-class="duration-150 ease-out"
115 enter-from-class="opacity-0 scale-102 !backdrop-blur-0px"
116 leave-active-class="duration-200 ease-in"
117 leave-to-class="opacity-0 scale-102 !backdrop-blur-0px"
118 >
119 <div
120 v-if="showOverview"
121 class="fixed left-0 right-0 top-0 h-[calc(var(--vh,1vh)*100)] z-modal bg-main !bg-opacity-75 p-16 py-20 overflow-y-auto backdrop-blur-5px select-none"
122 @click="close"
123 >
124 <div
125 class="grid gap-y-4 gap-x-8 w-full"
126 :style="`grid-template-columns: repeat(auto-fit,minmax(${cardWidth}px,1fr))`"
127 >
128 <div
129 v-for="(route, idx) of slides"
130 :key="route.no"
131 class="relative"
132 >
133 <div
134 class="inline-block border rounded overflow-hidden bg-main hover:border-primary transition"
135 :class="(focus(idx + 1) || currentOverviewPage === idx + 1) ? 'border-primary' : 'border-main'"
136 @click="go(route.no)"
137 >
138 <SlideContainer
139 :key="route.no"
140 :no="route.no"
141 :use-snapshot="true"
142 :width="cardWidth"
143 class="pointer-events-none"
144 >
145 <SlideWrapper
146 :clicks-context="createFixedClicks(route, CLICKS_MAX)"
147 :route="route"
148 render-context="overview"
149 />
150 <DrawingPreview :page="route.no" />
151 </SlideContainer>
152 </div>
153 <div
154 class="absolute top-0"
155 :style="`left: ${cardWidth + 5}px`"
156 >
157 <template v-if="keyboardBuffer && String(idx + 1).startsWith(keyboardBuffer)">
158 <span class="text-green font-bold">{{ keyboardBuffer }}</span>
159 <span class="opacity-50">{{ String(idx + 1).slice(keyboardBuffer.length) }}</span>
160 </template>
161 <span v-else class="opacity-50">
162 {{ idx + 1 }}
163 </span>
164 </div>
165 </div>
166 </div>
167 </div>
168 </Transition>
169 <div
170 v-show="showOverview"
171 class="fixed top-4 right-4 z-modal text-gray-400 flex flex-col items-center gap-2"
172 >
173 <IconButton title="Close" class="text-2xl" @click="close">
174 <div class="i-carbon:close" />
175 </IconButton>
176 <IconButton
177 v-if="__SLIDEV_FEATURE_PRESENTER__"
178 as="a"
179 title="Slides Overview"
180 target="_blank"
181 :href="`${pathPrefix}overview`"
182 tab-index="-1"
183 class="text-2xl"
184 >
185 <div class="i-carbon:list-boxes" />
186 </IconButton>
187 <IconButton
188 v-if="__DEV__ && isScreenshotSupported"
189 title="Capture slides as images"
190 class="text-2xl"
191 @click="captureSlidesOverview"
192 >
193 <div class="i-carbon:drop-photo" />
194 </IconButton>
195 </div>
196 </template>
197
197 lines Plain Text