@slidev/client 0.50.0-beta.10 → 0.50.0-beta.12
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/ExportPdfTip.vue +90 -0
- package/internals/FormCheckbox.vue +16 -0
- package/internals/FormItem.vue +41 -0
- package/internals/IconButton.vue +7 -2
- package/internals/NavControls.vue +9 -3
- package/internals/PrintContainer.vue +2 -21
- package/internals/PrintSlide.vue +4 -3
- package/internals/PrintSlideClick.vue +11 -3
- package/internals/Settings.vue +5 -2
- package/internals/SlidesShow.vue +7 -3
- package/logic/screenshot.ts +61 -0
- package/logic/shortcuts.ts +36 -35
- package/logic/slides.ts +2 -1
- package/modules/v-mark.ts +6 -0
- package/package.json +15 -13
- package/pages/export.vue +369 -0
- 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 +4 -3
- package/internals/PrintStyle.vue +0 -16
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
export async function startScreenshotSession(width: number, height: number) {
|
|
4
|
+
const canvas = document.createElement('canvas')
|
|
5
|
+
canvas.width = width
|
|
6
|
+
canvas.height = height
|
|
7
|
+
const context = canvas.getContext('2d')!
|
|
8
|
+
const video = document.createElement('video')
|
|
9
|
+
video.width = width
|
|
10
|
+
video.height = height
|
|
11
|
+
|
|
12
|
+
const captureStream = ref<MediaStream | null>(await navigator.mediaDevices.getDisplayMedia({
|
|
13
|
+
video: {
|
|
14
|
+
// Use a rather small frame rate
|
|
15
|
+
frameRate: 10,
|
|
16
|
+
// @ts-expect-error missing types
|
|
17
|
+
cursor: 'never',
|
|
18
|
+
},
|
|
19
|
+
selfBrowserSurface: 'include',
|
|
20
|
+
preferCurrentTab: true,
|
|
21
|
+
}))
|
|
22
|
+
captureStream.value!.addEventListener('inactive', dispose)
|
|
23
|
+
|
|
24
|
+
video.srcObject = captureStream.value!
|
|
25
|
+
video.play()
|
|
26
|
+
|
|
27
|
+
function screenshot(element: HTMLElement) {
|
|
28
|
+
if (!captureStream.value)
|
|
29
|
+
throw new Error('captureStream inactive')
|
|
30
|
+
context.clearRect(0, 0, width, height)
|
|
31
|
+
const { left, top, width: elWidth } = element.getBoundingClientRect()
|
|
32
|
+
context.drawImage(
|
|
33
|
+
video,
|
|
34
|
+
left * window.devicePixelRatio,
|
|
35
|
+
top * window.devicePixelRatio,
|
|
36
|
+
elWidth * window.devicePixelRatio,
|
|
37
|
+
elWidth / width * height * window.devicePixelRatio,
|
|
38
|
+
0,
|
|
39
|
+
0,
|
|
40
|
+
width,
|
|
41
|
+
height,
|
|
42
|
+
)
|
|
43
|
+
return canvas.toDataURL('image/png')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function dispose() {
|
|
47
|
+
captureStream.value?.getTracks().forEach(track => track.stop())
|
|
48
|
+
captureStream.value = null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
isActive: computed(() => !!captureStream.value),
|
|
53
|
+
screenshot,
|
|
54
|
+
dispose,
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type ScreenshotSession = Awaited<ReturnType<typeof startScreenshotSession>>
|
|
59
|
+
|
|
60
|
+
const chromeVersion = window.navigator.userAgent.match(/Chrome\/(\d+)/)?.[1]
|
|
61
|
+
export const isScreenshotSupported = chromeVersion ? Number(chromeVersion) >= 94 : false
|
package/logic/shortcuts.ts
CHANGED
|
@@ -4,46 +4,15 @@ import type { Ref } from 'vue'
|
|
|
4
4
|
import { onKeyStroke } from '@vueuse/core'
|
|
5
5
|
import { and, not } from '@vueuse/math'
|
|
6
6
|
import { watch } from 'vue'
|
|
7
|
+
import { useNav } from '../composables/useNav'
|
|
7
8
|
import setupShortcuts from '../setup/shortcuts'
|
|
8
9
|
import { fullscreen, isInputting, isOnFocus, magicKeys, shortcutsEnabled } from '../state'
|
|
9
10
|
|
|
10
|
-
const _shortcut = and(not(isInputting), not(isOnFocus), shortcutsEnabled)
|
|
11
|
-
|
|
12
|
-
export function shortcut(key: string | Ref<boolean>, fn: Fn, autoRepeat = false) {
|
|
13
|
-
if (typeof key === 'string')
|
|
14
|
-
key = magicKeys[key]
|
|
15
|
-
|
|
16
|
-
const source = and(key, _shortcut)
|
|
17
|
-
let count = 0
|
|
18
|
-
let timer: any
|
|
19
|
-
const trigger = () => {
|
|
20
|
-
clearTimeout(timer)
|
|
21
|
-
if (!source.value) {
|
|
22
|
-
count = 0
|
|
23
|
-
return
|
|
24
|
-
}
|
|
25
|
-
if (autoRepeat) {
|
|
26
|
-
timer = setTimeout(trigger, Math.max(1000 - count * 250, 150))
|
|
27
|
-
count++
|
|
28
|
-
}
|
|
29
|
-
fn()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return watch(source, trigger, { flush: 'sync' })
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function strokeShortcut(key: KeyFilter, fn: Fn) {
|
|
36
|
-
return onKeyStroke(key, (ev) => {
|
|
37
|
-
if (!_shortcut.value)
|
|
38
|
-
return
|
|
39
|
-
if (!ev.repeat)
|
|
40
|
-
fn()
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
44
11
|
export function registerShortcuts() {
|
|
45
|
-
const
|
|
12
|
+
const { isPrintMode } = useNav()
|
|
13
|
+
const enabled = and(not(isInputting), not(isOnFocus), not(isPrintMode), shortcutsEnabled)
|
|
46
14
|
|
|
15
|
+
const allShortcuts = setupShortcuts()
|
|
47
16
|
const shortcuts = new Map<string | Ref<boolean>, ShortcutOptions>(
|
|
48
17
|
allShortcuts.map((options: ShortcutOptions) => [options.key, options]),
|
|
49
18
|
)
|
|
@@ -54,4 +23,36 @@ export function registerShortcuts() {
|
|
|
54
23
|
})
|
|
55
24
|
|
|
56
25
|
strokeShortcut('f', () => fullscreen.toggle())
|
|
26
|
+
|
|
27
|
+
function shortcut(key: string | Ref<boolean>, fn: Fn, autoRepeat = false) {
|
|
28
|
+
if (typeof key === 'string')
|
|
29
|
+
key = magicKeys[key]
|
|
30
|
+
|
|
31
|
+
const source = and(key, enabled)
|
|
32
|
+
let count = 0
|
|
33
|
+
let timer: any
|
|
34
|
+
const trigger = () => {
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
if (!source.value) {
|
|
37
|
+
count = 0
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
if (autoRepeat) {
|
|
41
|
+
timer = setTimeout(trigger, Math.max(1000 - count * 250, 150))
|
|
42
|
+
count++
|
|
43
|
+
}
|
|
44
|
+
fn()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return watch(source, trigger, { flush: 'sync' })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function strokeShortcut(key: KeyFilter, fn: Fn) {
|
|
51
|
+
return onKeyStroke(key, (ev) => {
|
|
52
|
+
if (!enabled.value)
|
|
53
|
+
return
|
|
54
|
+
if (!ev.repeat)
|
|
55
|
+
fn()
|
|
56
|
+
})
|
|
57
|
+
}
|
|
57
58
|
}
|
package/logic/slides.ts
CHANGED
|
@@ -15,11 +15,12 @@ export function getSlide(no: number | string) {
|
|
|
15
15
|
export function getSlidePath(
|
|
16
16
|
route: SlideRoute | number | string,
|
|
17
17
|
presenter: boolean,
|
|
18
|
+
exporting: boolean = false,
|
|
18
19
|
) {
|
|
19
20
|
if (typeof route === 'number' || typeof route === 'string')
|
|
20
21
|
route = getSlide(route)!
|
|
21
22
|
const no = route.meta.slide?.frontmatter.routeAlias ?? route.no
|
|
22
|
-
return presenter ? `/presenter/${no}` : `/${no}`
|
|
23
|
+
return exporting ? `/export/${no}` : presenter ? `/presenter/${no}` : `/${no}`
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export function useIsSlideActive() {
|
package/modules/v-mark.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { RawAtValue } from '@slidev/types'
|
|
|
3
3
|
import type { App } from 'vue'
|
|
4
4
|
import { annotate } from '@slidev/rough-notation'
|
|
5
5
|
import { computed, watchEffect } from 'vue'
|
|
6
|
+
import { useNav } from '../composables/useNav'
|
|
6
7
|
import { resolveClick } from './v-click'
|
|
7
8
|
|
|
8
9
|
export interface RoughDirectiveOptions extends Partial<RoughAnnotationConfig> {
|
|
@@ -78,6 +79,8 @@ export function createVMarkDirective() {
|
|
|
78
79
|
name: 'v-mark',
|
|
79
80
|
|
|
80
81
|
mounted: (el, binding) => {
|
|
82
|
+
const { isPrintMode } = useNav()
|
|
83
|
+
|
|
81
84
|
const options = computed(() => {
|
|
82
85
|
const bindingOptions = (typeof binding.value === 'object' && !Array.isArray(binding.value))
|
|
83
86
|
? { ...binding.value }
|
|
@@ -109,6 +112,9 @@ export function createVMarkDirective() {
|
|
|
109
112
|
}
|
|
110
113
|
options.type ||= 'underline'
|
|
111
114
|
|
|
115
|
+
if (isPrintMode.value)
|
|
116
|
+
options.animationDuration = 1 /* millisecond */
|
|
117
|
+
|
|
112
118
|
return options
|
|
113
119
|
})
|
|
114
120
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@slidev/client",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.50.0-beta.
|
|
4
|
+
"version": "0.50.0-beta.12",
|
|
5
5
|
"description": "Presentation slides for developers",
|
|
6
6
|
"author": "antfu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"@iconify-json/carbon": "^1.2.4",
|
|
33
33
|
"@iconify-json/ph": "^1.2.1",
|
|
34
34
|
"@iconify-json/svg-spinners": "^1.2.1",
|
|
35
|
-
"@shikijs/monaco": "^1.24.
|
|
36
|
-
"@shikijs/vitepress-twoslash": "^1.24.
|
|
35
|
+
"@shikijs/monaco": "^1.24.2",
|
|
36
|
+
"@shikijs/vitepress-twoslash": "^1.24.2",
|
|
37
37
|
"@slidev/rough-notation": "^0.1.0",
|
|
38
38
|
"@typescript/ata": "^0.9.7",
|
|
39
|
-
"@unhead/vue": "^1.11.
|
|
40
|
-
"@unocss/reset": "^0.65.
|
|
39
|
+
"@unhead/vue": "^1.11.14",
|
|
40
|
+
"@unocss/reset": "^0.65.1",
|
|
41
41
|
"@vueuse/core": "^12.0.0",
|
|
42
42
|
"@vueuse/math": "^12.0.0",
|
|
43
43
|
"@vueuse/motion": "^2.2.6",
|
|
@@ -46,23 +46,25 @@
|
|
|
46
46
|
"floating-vue": "^5.2.2",
|
|
47
47
|
"fuse.js": "^7.0.0",
|
|
48
48
|
"html-to-image": "^1.11.11",
|
|
49
|
-
"katex": "^0.16.
|
|
49
|
+
"katex": "^0.16.15",
|
|
50
50
|
"lz-string": "^1.5.0",
|
|
51
51
|
"mermaid": "^11.4.1",
|
|
52
52
|
"monaco-editor": "0.51.0",
|
|
53
|
-
"
|
|
53
|
+
"nanotar": "^0.1.1",
|
|
54
|
+
"pptxgenjs": "^3.12.0",
|
|
55
|
+
"prettier": "^3.4.2",
|
|
54
56
|
"recordrtc": "^5.6.2",
|
|
55
|
-
"shiki": "^1.24.
|
|
56
|
-
"shiki-magic-move": "^0.5.
|
|
57
|
+
"shiki": "^1.24.2",
|
|
58
|
+
"shiki-magic-move": "^0.5.2",
|
|
57
59
|
"typescript": "5.6.3",
|
|
58
|
-
"unocss": "^0.65.
|
|
60
|
+
"unocss": "^0.65.1",
|
|
59
61
|
"vue": "^3.5.13",
|
|
60
62
|
"vue-router": "^4.5.0",
|
|
61
63
|
"yaml": "^2.6.1",
|
|
62
|
-
"@slidev/
|
|
63
|
-
"@slidev/
|
|
64
|
+
"@slidev/parser": "0.50.0-beta.12",
|
|
65
|
+
"@slidev/types": "0.50.0-beta.12"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
|
-
"vite": "^6.0.
|
|
68
|
+
"vite": "^6.0.3"
|
|
67
69
|
}
|
|
68
70
|
}
|
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 bg-main px2 py0 shadow z-1000 border="~ main rounded">
|
|
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/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"
|