@slidev/client 0.50.0-beta.8 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composables/useClicks.ts +6 -5
- package/composables/useDragElements.ts +30 -24
- package/composables/useNav.ts +17 -15
- package/composables/usePrintStyles.ts +28 -0
- package/constants.ts +1 -0
- package/internals/ClicksSlider.vue +1 -1
- package/internals/ContextMenu.vue +1 -1
- package/internals/DragControl.vue +1 -1
- package/internals/DrawingControls.vue +1 -1
- package/internals/ExportPdfTip.vue +90 -0
- package/internals/FormCheckbox.vue +16 -0
- package/internals/FormItem.vue +41 -0
- package/internals/IconButton.vue +7 -2
- package/internals/MenuButton.vue +1 -1
- package/internals/Modal.vue +1 -1
- package/internals/NavControls.vue +10 -4
- package/internals/PrintContainer.vue +2 -21
- package/internals/PrintSlide.vue +4 -3
- package/internals/PrintSlideClick.vue +11 -3
- package/internals/QuickOverview.vue +2 -2
- package/internals/Settings.vue +5 -2
- package/internals/SideEditor.vue +1 -1
- package/internals/SlidesShow.vue +7 -3
- package/internals/WebCamera.vue +2 -2
- package/layouts/error.vue +5 -1
- package/logic/dark.ts +11 -0
- package/logic/screenshot.ts +61 -0
- package/logic/shortcuts.ts +36 -35
- package/logic/slides.ts +2 -1
- package/main.ts +7 -3
- package/modules/v-mark.ts +6 -0
- package/package.json +18 -16
- package/pages/export.vue +369 -0
- package/pages/overview.vue +1 -1
- package/pages/play.vue +1 -4
- package/pages/presenter.vue +9 -0
- package/pages/print.vue +0 -2
- package/setup/monaco.ts +14 -14
- package/setup/root.ts +6 -2
- package/setup/routes.ts +23 -12
- package/state/index.ts +2 -1
- package/styles/index.css +9 -4
- package/uno.config.ts +14 -0
- package/internals/PrintStyle.vue +0 -16
package/pages/export.vue
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { ScreenshotSession } from '../logic/screenshot'
|
|
3
|
+
import { sleep } from '@antfu/utils'
|
|
4
|
+
import { parseRangeString } from '@slidev/parser/utils'
|
|
5
|
+
import { useHead } from '@unhead/vue'
|
|
6
|
+
import { provideLocal, useElementSize, useLocalStorage, useStyleTag, watchDebounced } from '@vueuse/core'
|
|
7
|
+
|
|
8
|
+
import { computed, ref, useTemplateRef, watch } from 'vue'
|
|
9
|
+
import { useRouter } from 'vue-router'
|
|
10
|
+
import { useDarkMode } from '../composables/useDarkMode'
|
|
11
|
+
import { useNav } from '../composables/useNav'
|
|
12
|
+
import { patchMonacoColors } from '../composables/usePrintStyles'
|
|
13
|
+
import { injectionSlideScale } from '../constants'
|
|
14
|
+
import { configs, slideHeight, slidesTitle, slideWidth } from '../env'
|
|
15
|
+
import ExportPdfTip from '../internals/ExportPdfTip.vue'
|
|
16
|
+
import FormCheckbox from '../internals/FormCheckbox.vue'
|
|
17
|
+
import FormItem from '../internals/FormItem.vue'
|
|
18
|
+
import PrintSlide from '../internals/PrintSlide.vue'
|
|
19
|
+
import { isScreenshotSupported, startScreenshotSession } from '../logic/screenshot'
|
|
20
|
+
import { skipExportPdfTip } from '../state'
|
|
21
|
+
import Play from './play.vue'
|
|
22
|
+
|
|
23
|
+
const { slides, isPrintWithClicks, hasNext, go, next, currentSlideNo, clicks, printRange } = useNav()
|
|
24
|
+
const router = useRouter()
|
|
25
|
+
const { isColorSchemaConfigured, isDark } = useDarkMode()
|
|
26
|
+
const { width: containerWidth } = useElementSize(useTemplateRef('export-container'))
|
|
27
|
+
const { height: contentHeight } = useElementSize(useTemplateRef('export-content'))
|
|
28
|
+
const scale = computed(() => containerWidth.value / slideWidth.value)
|
|
29
|
+
const contentMarginBottom = computed(() => `${contentHeight.value * (scale.value - 1)}px`)
|
|
30
|
+
const rangesRaw = ref('')
|
|
31
|
+
const initialWait = ref(1000)
|
|
32
|
+
const delay = useLocalStorage('slidev-export-capture-delay', 400, { listenToStorageChanges: false })
|
|
33
|
+
type ScreenshotResult = { slideIndex: number, clickIndex: number, dataUrl: string }[]
|
|
34
|
+
const screenshotSession = ref<ScreenshotSession | null>(null)
|
|
35
|
+
const capturedImages = ref<ScreenshotResult | null>(null)
|
|
36
|
+
const title = ref(configs.exportFilename || slidesTitle)
|
|
37
|
+
|
|
38
|
+
useHead({
|
|
39
|
+
title,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
provideLocal(injectionSlideScale, scale)
|
|
43
|
+
|
|
44
|
+
const showExportPdfTip = ref(false)
|
|
45
|
+
function pdf() {
|
|
46
|
+
if (skipExportPdfTip.value) {
|
|
47
|
+
doPrint()
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
showExportPdfTip.value = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function doPrint() {
|
|
55
|
+
patchMonacoColors()
|
|
56
|
+
setTimeout(window.print, 100)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function capturePngs() {
|
|
60
|
+
if (screenshotSession.value) {
|
|
61
|
+
screenshotSession.value.dispose()
|
|
62
|
+
screenshotSession.value = null
|
|
63
|
+
}
|
|
64
|
+
if (capturedImages.value)
|
|
65
|
+
return capturedImages.value
|
|
66
|
+
try {
|
|
67
|
+
const scale = 2
|
|
68
|
+
screenshotSession.value = await startScreenshotSession(slideWidth.value * scale, slideHeight.value * scale)
|
|
69
|
+
const result: ScreenshotResult = []
|
|
70
|
+
|
|
71
|
+
go(1, 0, true)
|
|
72
|
+
|
|
73
|
+
await sleep(initialWait.value + delay.value)
|
|
74
|
+
while (true) {
|
|
75
|
+
if (!screenshotSession.value) {
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
result.push({
|
|
79
|
+
slideIndex: currentSlideNo.value - 1,
|
|
80
|
+
clickIndex: clicks.value,
|
|
81
|
+
dataUrl: screenshotSession.value.screenshot(document.getElementById('slide-content')!),
|
|
82
|
+
})
|
|
83
|
+
if (hasNext.value) {
|
|
84
|
+
await sleep(delay.value)
|
|
85
|
+
next()
|
|
86
|
+
await sleep(delay.value)
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
break
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (screenshotSession.value) {
|
|
94
|
+
screenshotSession.value.dispose()
|
|
95
|
+
capturedImages.value = result
|
|
96
|
+
screenshotSession.value = null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (e) {
|
|
100
|
+
console.error(e)
|
|
101
|
+
capturedImages.value = null
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
router.push('/export')
|
|
105
|
+
}
|
|
106
|
+
return capturedImages.value
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function pptx() {
|
|
110
|
+
const pngs = await capturePngs()
|
|
111
|
+
if (!pngs)
|
|
112
|
+
return
|
|
113
|
+
const pptx = await import('pptxgenjs')
|
|
114
|
+
.then(r => r.default)
|
|
115
|
+
.then(PptxGen => new PptxGen())
|
|
116
|
+
|
|
117
|
+
const layoutName = `${slideWidth.value}x${slideHeight.value}`
|
|
118
|
+
pptx.defineLayout({
|
|
119
|
+
name: layoutName,
|
|
120
|
+
width: slideWidth.value / 96,
|
|
121
|
+
height: slideHeight.value / 96,
|
|
122
|
+
})
|
|
123
|
+
pptx.layout = layoutName
|
|
124
|
+
if (configs.author)
|
|
125
|
+
pptx.author = configs.author
|
|
126
|
+
pptx.company = 'Created using Slidev'
|
|
127
|
+
pptx.title = title.value
|
|
128
|
+
if (typeof configs.info === 'string')
|
|
129
|
+
pptx.subject = configs.info
|
|
130
|
+
|
|
131
|
+
pngs.forEach(({ slideIndex, dataUrl }) => {
|
|
132
|
+
const slide = pptx.addSlide()
|
|
133
|
+
slide.background = {
|
|
134
|
+
data: dataUrl,
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const note = slides.value[slideIndex].meta.slide.note
|
|
138
|
+
if (note)
|
|
139
|
+
slide.addNotes(note)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const blob = await pptx.write({
|
|
143
|
+
outputType: 'blob',
|
|
144
|
+
compression: true,
|
|
145
|
+
}) as Blob
|
|
146
|
+
const url = URL.createObjectURL(blob)
|
|
147
|
+
const a = document.createElement('a')
|
|
148
|
+
a.href = url
|
|
149
|
+
a.download = `${title.value}.pptx`
|
|
150
|
+
a.click()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function pngsGz() {
|
|
154
|
+
const pngs = await capturePngs()
|
|
155
|
+
if (!pngs)
|
|
156
|
+
return
|
|
157
|
+
const { createTarGzip } = await import('nanotar')
|
|
158
|
+
const data = await createTarGzip(
|
|
159
|
+
pngs.map(({ slideIndex, dataUrl }) => ({
|
|
160
|
+
name: `${slideIndex}.png`,
|
|
161
|
+
data: new Uint8Array(atob(dataUrl.split(',')[1]).split('').map(char => char.charCodeAt(0))),
|
|
162
|
+
})),
|
|
163
|
+
)
|
|
164
|
+
const a = document.createElement('a')
|
|
165
|
+
const blob = new Blob([data], { type: 'application/gzip' })
|
|
166
|
+
a.href = URL.createObjectURL(blob)
|
|
167
|
+
a.download = `${title.value}.tar.gz`
|
|
168
|
+
a.click()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
useStyleTag(computed(() => screenshotSession.value?.isActive
|
|
172
|
+
? `
|
|
173
|
+
html {
|
|
174
|
+
cursor: none;
|
|
175
|
+
margin-bottom: 20px;
|
|
176
|
+
}
|
|
177
|
+
body {
|
|
178
|
+
pointer-events: none;
|
|
179
|
+
}`
|
|
180
|
+
: `
|
|
181
|
+
:root {
|
|
182
|
+
--slidev-slide-scale: ${scale.value};
|
|
183
|
+
}
|
|
184
|
+
`))
|
|
185
|
+
|
|
186
|
+
// clear captured images when settings changed
|
|
187
|
+
watch(
|
|
188
|
+
[
|
|
189
|
+
isDark,
|
|
190
|
+
printRange,
|
|
191
|
+
isPrintWithClicks,
|
|
192
|
+
],
|
|
193
|
+
() => capturedImages.value = null,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
watchDebounced(
|
|
197
|
+
[slides, rangesRaw],
|
|
198
|
+
() => printRange.value = parseRangeString(slides.value.length, rangesRaw.value),
|
|
199
|
+
{ debounce: 300 },
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
// clear captured images when HMR
|
|
203
|
+
if (import.meta.hot) {
|
|
204
|
+
import.meta.hot.on('vite:beforeUpdate', () => {
|
|
205
|
+
capturedImages.value = null
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
</script>
|
|
209
|
+
|
|
210
|
+
<template>
|
|
211
|
+
<Play v-if="screenshotSession?.isActive" />
|
|
212
|
+
<div
|
|
213
|
+
v-else
|
|
214
|
+
class="fixed inset-0 flex flex-col md:flex-row md:gap-8 print:position-unset print:inset-0 print:block print:min-h-max justify-center of-hidden bg-main"
|
|
215
|
+
>
|
|
216
|
+
<div class="print:hidden min-w-fit flex flex-wrap md:flex-nowrap md:of-y-auto md:flex-col gap-2 p-6 max-w-100">
|
|
217
|
+
<h1 class="text-3xl md:my-4 flex items-center gap-2 w-full">
|
|
218
|
+
<RouterLink to="/" class="i-carbon:previous-outline op-70 hover:op-100" />
|
|
219
|
+
Browser Exporter
|
|
220
|
+
<sup op50 italic text-sm>Experimental</sup>
|
|
221
|
+
</h1>
|
|
222
|
+
<div flex="~ col gap-2">
|
|
223
|
+
<h2>Options</h2>
|
|
224
|
+
<FormItem title="Title">
|
|
225
|
+
<input v-model="title" type="text">
|
|
226
|
+
</FormItem>
|
|
227
|
+
<FormItem title="Range">
|
|
228
|
+
<input v-model="rangesRaw" type="text" :placeholder="`1-${slides.length}`">
|
|
229
|
+
</FormItem>
|
|
230
|
+
<FormItem title="Dark mode">
|
|
231
|
+
<FormCheckbox v-model="isDark" :disabled="isColorSchemaConfigured" />
|
|
232
|
+
</FormItem>
|
|
233
|
+
<FormItem title="With clicks">
|
|
234
|
+
<FormCheckbox v-model="isPrintWithClicks" />
|
|
235
|
+
</FormItem>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="flex-grow" />
|
|
238
|
+
<div class="min-w-fit" flex="~ col gap-3">
|
|
239
|
+
<div border="~ main rounded-lg" p3 flex="~ col gap-2">
|
|
240
|
+
<h2>Export as Vector File</h2>
|
|
241
|
+
<div class="flex flex-col gap-2 items-start min-w-max">
|
|
242
|
+
<button @click="pdf">
|
|
243
|
+
PDF
|
|
244
|
+
</button>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div border="~ main rounded-lg" p3 flex="~ col gap-2" :class="isScreenshotSupported ? '' : 'border-orange'">
|
|
249
|
+
<h2>Export as Images</h2>
|
|
250
|
+
<div v-if="!isScreenshotSupported" class="min-w-full w-0 text-orange/100 p-1 mb--4 bg-orange/10 rounded">
|
|
251
|
+
<span class="i-carbon:warning-alt inline-block mb--.5" />
|
|
252
|
+
Your browser may not support image capturing.
|
|
253
|
+
If you encounter issues, please use a modern Chromium-based browser,
|
|
254
|
+
or export via the CLI.
|
|
255
|
+
</div>
|
|
256
|
+
<div class="flex flex-col gap-2 items-start min-w-max">
|
|
257
|
+
<button @click="pptx">
|
|
258
|
+
PPTX
|
|
259
|
+
</button>
|
|
260
|
+
<button @click="pngsGz">
|
|
261
|
+
PNGs.gz
|
|
262
|
+
</button>
|
|
263
|
+
</div>
|
|
264
|
+
<div w-full h-1px border="t main" my2 />
|
|
265
|
+
<div class="relative flex flex-col gap-2 flex-nowrap">
|
|
266
|
+
<div class="flex flex-col gap-2 items-start min-w-max">
|
|
267
|
+
<button v-if="capturedImages" class="flex justify-center items-center gap-2" @click="capturedImages = null">
|
|
268
|
+
<span class="i-carbon:trash-can inline-block text-xl" />
|
|
269
|
+
Clear Captured Images
|
|
270
|
+
</button>
|
|
271
|
+
<button v-else class="flex justify-center items-center gap-2" @click="capturePngs">
|
|
272
|
+
<div class="i-carbon:camera-action inline-block text-xl" />
|
|
273
|
+
Pre-capture Slides as Images
|
|
274
|
+
</button>
|
|
275
|
+
<FormItem title="Delay" description="Delay between capturing each slide in milliseconds.<br>Increase this value if slides are captured incompletely. <br>(Not related to PDF export)">
|
|
276
|
+
<input v-model="delay" type="number" step="50" min="50">
|
|
277
|
+
</FormItem>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
<div id="export-container" ref="export-container" relative>
|
|
284
|
+
<div print:hidden fixed right-5 bottom-5 px2 py0 z-label slidev-glass-effect>
|
|
285
|
+
<span op75>Rendering as {{ capturedImages ? 'Captured Images' : 'DOM' }} </span>
|
|
286
|
+
</div>
|
|
287
|
+
<div v-show="!capturedImages" id="export-content" ref="export-content">
|
|
288
|
+
<PrintSlide v-for="route, index in slides" :key="index" :hidden="!printRange.includes(index + 1)" :route />
|
|
289
|
+
</div>
|
|
290
|
+
<div v-if="capturedImages" id="export-content-images" class="print:hidden grid">
|
|
291
|
+
<div v-for="png, i of capturedImages" :key="i" class="print-slide-container">
|
|
292
|
+
<img :src="png.dataUrl">
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
<div id="twoslash-container" />
|
|
297
|
+
<ExportPdfTip v-model="showExportPdfTip" @print="doPrint" />
|
|
298
|
+
</div>
|
|
299
|
+
</template>
|
|
300
|
+
|
|
301
|
+
<style scoped>
|
|
302
|
+
@media not print {
|
|
303
|
+
#export-container {
|
|
304
|
+
scrollbar-width: thin;
|
|
305
|
+
scroll-behavior: smooth;
|
|
306
|
+
--uno: w-full overflow-x-hidden overflow-y-auto max-h-full max-w-300 p-6;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
#export-content {
|
|
310
|
+
transform: v-bind('`scale(${scale})`');
|
|
311
|
+
margin-bottom: v-bind('contentMarginBottom');
|
|
312
|
+
--uno: origin-tl;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#export-content,
|
|
316
|
+
#export-content-images {
|
|
317
|
+
--uno: flex flex-col gap-2;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@media print {
|
|
322
|
+
#export-content {
|
|
323
|
+
transform: scale(1);
|
|
324
|
+
display: block !important;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
button {
|
|
329
|
+
--uno: 'w-full rounded bg-gray:10 px-4 py-2 hover:bg-gray/20';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
label {
|
|
333
|
+
--uno: text-xl flex gap-2 items-center select-none;
|
|
334
|
+
|
|
335
|
+
span {
|
|
336
|
+
--uno: flex-grow;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
input[type='text'],
|
|
340
|
+
input[type='number'] {
|
|
341
|
+
--uno: border border-main rounded px-2 py-1;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
h2 {
|
|
346
|
+
--uno: font-500 op-70;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
#export-content {
|
|
350
|
+
--uno: pointer-events-none;
|
|
351
|
+
}
|
|
352
|
+
</style>
|
|
353
|
+
|
|
354
|
+
<style>
|
|
355
|
+
@media print {
|
|
356
|
+
html,
|
|
357
|
+
body,
|
|
358
|
+
#app {
|
|
359
|
+
overflow: unset !important;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
@media not print {
|
|
364
|
+
#export-content-images .print-slide-container,
|
|
365
|
+
#export-content .print-slide-container {
|
|
366
|
+
--uno: border border-main rounded-md shadow of-hidden;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
</style>
|
package/pages/overview.vue
CHANGED
|
@@ -135,7 +135,7 @@ onMounted(() => {
|
|
|
135
135
|
</button>
|
|
136
136
|
<div
|
|
137
137
|
v-if="route.meta?.slide?.title"
|
|
138
|
-
class="pointer-events-none select-none absolute left-110%
|
|
138
|
+
class="pointer-events-none select-none absolute left-110% top-50% translate-y--50% ws-nowrap z-label px2 slidev-glass-effect transition duration-400 op0 group-hover:op100"
|
|
139
139
|
:class="activeBlocks.includes(idx) ? 'text-primary' : 'text-main important-text-op-50'"
|
|
140
140
|
>
|
|
141
141
|
{{ route.meta?.slide?.title }}
|
package/pages/play.vue
CHANGED
|
@@ -8,12 +8,11 @@ import { useWakeLock } from '../composables/useWakeLock'
|
|
|
8
8
|
import Controls from '../internals/Controls.vue'
|
|
9
9
|
import NavControls from '../internals/NavControls.vue'
|
|
10
10
|
import PresenterMouse from '../internals/PresenterMouse.vue'
|
|
11
|
-
import PrintStyle from '../internals/PrintStyle.vue'
|
|
12
11
|
import SlideContainer from '../internals/SlideContainer.vue'
|
|
13
12
|
import SlidesShow from '../internals/SlidesShow.vue'
|
|
14
13
|
import { onContextMenu } from '../logic/contextMenu'
|
|
15
14
|
import { registerShortcuts } from '../logic/shortcuts'
|
|
16
|
-
import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor
|
|
15
|
+
import { editorHeight, editorWidth, isEditorVertical, isScreenVertical, showEditor } from '../state'
|
|
17
16
|
|
|
18
17
|
const { next, prev, isPrintMode } = useNav()
|
|
19
18
|
const { isDrawing } = useDrawings()
|
|
@@ -63,14 +62,12 @@ if (__DEV__ && __SLIDEV_FEATURE_EDITOR__)
|
|
|
63
62
|
</script>
|
|
64
63
|
|
|
65
64
|
<template>
|
|
66
|
-
<PrintStyle v-if="isPrintMode" />
|
|
67
65
|
<div
|
|
68
66
|
id="page-root" ref="root" class="grid"
|
|
69
67
|
:class="isEditorVertical ? 'grid-rows-[1fr_max-content]' : 'grid-cols-[1fr_max-content]'"
|
|
70
68
|
>
|
|
71
69
|
<SlideContainer
|
|
72
70
|
:style="{ background: 'var(--slidev-slide-container-background, black)' }"
|
|
73
|
-
:width="isPrintMode ? windowSize.width.value : undefined"
|
|
74
71
|
is-main
|
|
75
72
|
@pointerdown="onClick"
|
|
76
73
|
@contextmenu="onContextMenu"
|
package/pages/presenter.vue
CHANGED
|
@@ -247,6 +247,15 @@ onMounted(() => {
|
|
|
247
247
|
'bottom bottom';
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
+
.grid-container.layout3 {
|
|
251
|
+
grid-template-columns: 2fr 3fr;
|
|
252
|
+
grid-template-rows: 1fr 1fr min-content;
|
|
253
|
+
grid-template-areas:
|
|
254
|
+
'note next'
|
|
255
|
+
'main next'
|
|
256
|
+
'bottom bottom';
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
@media (max-aspect-ratio: 3/5) {
|
|
251
260
|
.grid-container.layout1 {
|
|
252
261
|
grid-template-columns: 1fr;
|
package/pages/print.vue
CHANGED
|
@@ -3,7 +3,6 @@ import { recomputeAllPoppers } from 'floating-vue'
|
|
|
3
3
|
import { onMounted, watchEffect } from 'vue'
|
|
4
4
|
import { useNav } from '../composables/useNav'
|
|
5
5
|
import PrintContainer from '../internals/PrintContainer.vue'
|
|
6
|
-
import PrintStyle from '../internals/PrintStyle.vue'
|
|
7
6
|
import { windowSize } from '../state'
|
|
8
7
|
|
|
9
8
|
const { isPrintMode } = useNav()
|
|
@@ -21,7 +20,6 @@ onMounted(() => {
|
|
|
21
20
|
</script>
|
|
22
21
|
|
|
23
22
|
<template>
|
|
24
|
-
<PrintStyle v-if="isPrintMode" />
|
|
25
23
|
<div id="page-root" class="grid grid-cols-[1fr_max-content]">
|
|
26
24
|
<PrintContainer
|
|
27
25
|
class="w-full h-full"
|
package/setup/monaco.ts
CHANGED
|
@@ -64,22 +64,22 @@ const setup = createSingletonPromise(async () => {
|
|
|
64
64
|
|
|
65
65
|
const ata = configs.monacoTypesSource === 'cdn'
|
|
66
66
|
? setupTypeAcquisition({
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
67
|
+
projectName: 'TypeScript Playground',
|
|
68
|
+
typescript: ts as any, // Version mismatch. No problem found so far.
|
|
69
|
+
logger: console,
|
|
70
|
+
delegate: {
|
|
71
|
+
receivedFile: (code: string, path: string) => {
|
|
72
|
+
defaults.addExtraLib(code, `file://${path}`)
|
|
73
|
+
const uri = monaco.Uri.file(path)
|
|
74
|
+
if (monaco.editor.getModel(uri) === null)
|
|
75
|
+
monaco.editor.createModel(code, 'javascript', uri)
|
|
76
|
+
},
|
|
77
|
+
progress: (downloaded: number, total: number) => {
|
|
78
78
|
// eslint-disable-next-line no-console
|
|
79
|
-
|
|
79
|
+
console.debug(`[Typescript ATA] ${downloaded} / ${total}`)
|
|
80
|
+
},
|
|
80
81
|
},
|
|
81
|
-
}
|
|
82
|
-
})
|
|
82
|
+
})
|
|
83
83
|
: () => { }
|
|
84
84
|
|
|
85
85
|
monaco.languages.register({ id: 'vue' })
|
package/setup/root.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { useRouter } from 'vue-router'
|
|
|
5
5
|
import { createFixedClicks } from '../composables/useClicks'
|
|
6
6
|
import { useEmbeddedControl } from '../composables/useEmbeddedCtrl'
|
|
7
7
|
import { useNav } from '../composables/useNav'
|
|
8
|
+
import { usePrintStyles } from '../composables/usePrintStyles'
|
|
8
9
|
import { injectionClicksContext, injectionCurrentPage, injectionRenderContext, injectionSlidevContext, TRUST_ORIGINS } from '../constants'
|
|
9
10
|
import { configs, slidesTitle } from '../env'
|
|
10
11
|
import { skipTransition } from '../logic/hmr'
|
|
@@ -43,6 +44,7 @@ export default function setupRoot() {
|
|
|
43
44
|
hasPrimarySlide,
|
|
44
45
|
isNotesViewer,
|
|
45
46
|
isPresenter,
|
|
47
|
+
isPrintMode,
|
|
46
48
|
} = useNav()
|
|
47
49
|
|
|
48
50
|
useHead({
|
|
@@ -50,6 +52,8 @@ export default function setupRoot() {
|
|
|
50
52
|
htmlAttrs: configs.htmlAttrs,
|
|
51
53
|
})
|
|
52
54
|
|
|
55
|
+
usePrintStyles()
|
|
56
|
+
|
|
53
57
|
initSharedState(`${slidesTitle} - shared`)
|
|
54
58
|
initDrawingState(`${slidesTitle} - drawings`)
|
|
55
59
|
|
|
@@ -57,7 +61,7 @@ export default function setupRoot() {
|
|
|
57
61
|
|
|
58
62
|
// update shared state
|
|
59
63
|
function updateSharedState() {
|
|
60
|
-
if (isNotesViewer.value)
|
|
64
|
+
if (isNotesViewer.value || isPrintMode.value)
|
|
61
65
|
return
|
|
62
66
|
|
|
63
67
|
// we allow Presenter mode, or Viewer mode from trusted origins to update the shared state
|
|
@@ -86,7 +90,7 @@ export default function setupRoot() {
|
|
|
86
90
|
watch(clicksContext, updateSharedState)
|
|
87
91
|
|
|
88
92
|
onPatch((state) => {
|
|
89
|
-
if (!hasPrimarySlide.value)
|
|
93
|
+
if (!hasPrimarySlide.value || isPrintMode.value)
|
|
90
94
|
return
|
|
91
95
|
if (state.lastUpdate?.type === 'presenter' && (+state.page !== +currentSlideNo.value || +clicksContext.value.current !== +state.clicks)) {
|
|
92
96
|
skipTransition.value = false
|
package/setup/routes.ts
CHANGED
|
@@ -5,21 +5,21 @@ import setups from '#slidev/setups/routes'
|
|
|
5
5
|
export default function setupRoutes() {
|
|
6
6
|
const routes: RouteRecordRaw[] = []
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
function passwordGuard(to: RouteLocationNormalized) {
|
|
9
|
+
if (!configs.remote || configs.remote === to.query.password)
|
|
10
|
+
return true
|
|
11
|
+
if (configs.remote && to.query.password === undefined) {
|
|
12
|
+
// eslint-disable-next-line no-alert
|
|
13
|
+
const password = prompt('Enter password')
|
|
14
|
+
if (configs.remote === password)
|
|
11
15
|
return true
|
|
12
|
-
if (configs.remote && to.query.password === undefined) {
|
|
13
|
-
// eslint-disable-next-line no-alert
|
|
14
|
-
const password = prompt('Enter password')
|
|
15
|
-
if (configs.remote === password)
|
|
16
|
-
return true
|
|
17
|
-
}
|
|
18
|
-
if (to.params.no)
|
|
19
|
-
return { path: `/${to.params.no}` }
|
|
20
|
-
return { path: '' }
|
|
21
16
|
}
|
|
17
|
+
if (to.params.no)
|
|
18
|
+
return { path: `/${to.params.no}` }
|
|
19
|
+
return { path: '' }
|
|
20
|
+
}
|
|
22
21
|
|
|
22
|
+
if (__SLIDEV_FEATURE_PRESENTER__) {
|
|
23
23
|
routes.push(
|
|
24
24
|
{
|
|
25
25
|
name: 'entry',
|
|
@@ -64,6 +64,17 @@ export default function setupRoutes() {
|
|
|
64
64
|
)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
if (__SLIDEV_FEATURE_BROWSER_EXPORTER__) {
|
|
68
|
+
routes.push(
|
|
69
|
+
{
|
|
70
|
+
name: 'export',
|
|
71
|
+
path: '/export/:no?',
|
|
72
|
+
component: () => import('../pages/export.vue'),
|
|
73
|
+
beforeEnter: passwordGuard,
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
routes.push(
|
|
68
79
|
{
|
|
69
80
|
name: 'play',
|
package/state/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export const currentCamera = useLocalStorage<string>('slidev-camera', 'default',
|
|
|
26
26
|
export const currentMic = useLocalStorage<string>('slidev-mic', 'default', { listenToStorageChanges: false })
|
|
27
27
|
export const slideScale = useLocalStorage<number>('slidev-scale', 0)
|
|
28
28
|
export const wakeLockEnabled = useLocalStorage('slidev-wake-lock', true)
|
|
29
|
+
export const skipExportPdfTip = useLocalStorage('slidev-skip-export-pdf-tip', false)
|
|
29
30
|
|
|
30
31
|
export const showPresenterCursor = useLocalStorage('slidev-presenter-cursor', true, { listenToStorageChanges: false })
|
|
31
32
|
export const showEditor = useLocalStorage('slidev-show-editor', false, { listenToStorageChanges: false })
|
|
@@ -40,7 +41,7 @@ export const presenterLayout = useLocalStorage('slidev-presenter-layout', 1, { l
|
|
|
40
41
|
|
|
41
42
|
export function togglePresenterLayout() {
|
|
42
43
|
presenterLayout.value = presenterLayout.value + 1
|
|
43
|
-
if (presenterLayout.value >
|
|
44
|
+
if (presenterLayout.value > 3)
|
|
44
45
|
presenterLayout.value = 1
|
|
45
46
|
}
|
|
46
47
|
|
package/styles/index.css
CHANGED
|
@@ -8,7 +8,9 @@ body,
|
|
|
8
8
|
height: 100vh;
|
|
9
9
|
height: calc(var(--vh, 1vh) * 100);
|
|
10
10
|
overflow: hidden;
|
|
11
|
-
|
|
11
|
+
print-color-adjust: exact;
|
|
12
|
+
-webkit-print-color-adjust: exact;
|
|
13
|
+
--uno: font-sans bg-main;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
html {
|
|
@@ -17,11 +19,10 @@ html {
|
|
|
17
19
|
|
|
18
20
|
.slidev-icon-btn {
|
|
19
21
|
aspect-ratio: 1;
|
|
20
|
-
display: inline-block;
|
|
21
22
|
user-select: none;
|
|
22
23
|
outline: none;
|
|
23
24
|
cursor: pointer;
|
|
24
|
-
@apply opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
|
|
25
|
+
@apply inline-flex items-center justify-center opacity-75 transition duration-200 ease-in-out align-middle rounded p-1;
|
|
25
26
|
@apply hover:(opacity-100 bg-gray-400 bg-opacity-10);
|
|
26
27
|
@apply focus-visible:(opacity-100 outline outline-2 outline-offset-2 outline-black dark:outline-white);
|
|
27
28
|
@apply md:p-2;
|
|
@@ -40,6 +41,10 @@ html {
|
|
|
40
41
|
pointer-events: none;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
.slidev-layout a.slidev-icon-btn {
|
|
45
|
+
@apply border-none hover:border-none hover:text-white;
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
.slidev-vclick-target {
|
|
44
49
|
@apply transition-opacity duration-100;
|
|
45
50
|
}
|
|
@@ -125,7 +130,7 @@ html {
|
|
|
125
130
|
position: fixed;
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
#twoslash-container .v-popper__wrapper {
|
|
133
|
+
#twoslash-container .v-popper__wrapper:not(.no-slide-scale > *) {
|
|
129
134
|
transform: scale(calc(1 * var(--slidev-slide-scale)));
|
|
130
135
|
transform-origin: 30px top;
|
|
131
136
|
}
|
package/uno.config.ts
CHANGED
|
@@ -13,6 +13,9 @@ export default defineConfig({
|
|
|
13
13
|
safelist: [
|
|
14
14
|
'!opacity-0',
|
|
15
15
|
'prose',
|
|
16
|
+
// See https://github.com/slidevjs/slidev/issues/1705
|
|
17
|
+
'grid-rows-[1fr_max-content]',
|
|
18
|
+
'grid-cols-[1fr_max-content]',
|
|
16
19
|
],
|
|
17
20
|
shortcuts: {
|
|
18
21
|
'bg-main': 'bg-white dark:bg-[#121212]',
|
|
@@ -27,6 +30,17 @@ export default defineConfig({
|
|
|
27
30
|
'abs-b': 'absolute bottom-0 left-0 right-0',
|
|
28
31
|
'abs-bl': 'absolute bottom-0 left-0',
|
|
29
32
|
'abs-br': 'absolute bottom-0 right-0',
|
|
33
|
+
|
|
34
|
+
'z-drawing': 'z-10',
|
|
35
|
+
'z-camera': 'z-15',
|
|
36
|
+
'z-dragging': 'z-18',
|
|
37
|
+
'z-menu': 'z-20',
|
|
38
|
+
'z-label': 'z-40',
|
|
39
|
+
'z-nav': 'z-50',
|
|
40
|
+
'z-context-menu': 'z-60',
|
|
41
|
+
'z-modal': 'z-70',
|
|
42
|
+
|
|
43
|
+
'slidev-glass-effect': 'shadow-xl backdrop-blur-8 border border-main bg-main bg-opacity-75!',
|
|
30
44
|
},
|
|
31
45
|
// Slidev Specific Variants, probably extrat to a preset later
|
|
32
46
|
variants: [
|
package/internals/PrintStyle.vue
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Slots } from 'vue'
|
|
3
|
-
import { h } from 'vue'
|
|
4
|
-
import { slideHeight, slideWidth } from '../env'
|
|
5
|
-
|
|
6
|
-
function vStyle<Props>(props: Props, { slots }: { slots: Slots }) {
|
|
7
|
-
if (slots.default)
|
|
8
|
-
return h('style', slots.default())
|
|
9
|
-
}
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<template>
|
|
13
|
-
<vStyle>
|
|
14
|
-
@page { size: {{ slideWidth }}px {{ slideHeight }}px; margin: 0px; }
|
|
15
|
-
</vStyle>
|
|
16
|
-
</template>
|