@slidev/client 0.50.0 → 0.51.0-beta.2
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/builtin/VSwitch.ts +3 -3
- package/composables/useClicks.ts +1 -1
- package/composables/useHideCursorIdle.ts +35 -0
- package/composables/useNav.ts +3 -3
- package/composables/useTimer.ts +1 -1
- package/composables/useWakeLock.ts +10 -6
- package/constants.ts +0 -1
- package/internals/Badge.vue +48 -0
- package/internals/{DevicesList.vue → DevicesSelectors.vue} +14 -4
- package/internals/FormItem.vue +6 -3
- package/internals/FormSlider.vue +68 -0
- package/internals/MenuButton.vue +2 -2
- package/internals/NavControls.vue +16 -9
- package/internals/QuickOverview.vue +24 -4
- package/internals/RecordingControls.vue +2 -2
- package/internals/RecordingDialog.vue +4 -14
- package/internals/ScreenCaptureMirror.vue +45 -0
- package/internals/SegmentControl.vue +29 -0
- package/internals/SelectList.vue +1 -9
- package/internals/Settings.vue +104 -30
- package/internals/SlideContainer.vue +33 -18
- package/internals/SlidesShow.vue +4 -5
- package/internals/SyncControls.vue +103 -0
- package/logic/color.ts +64 -0
- package/logic/snapshot.ts +73 -52
- package/package.json +12 -13
- package/pages/export.vue +24 -22
- package/pages/notes.vue +3 -3
- package/pages/play.vue +24 -2
- package/pages/presenter.vue +37 -18
- package/setup/root.ts +31 -24
- package/state/drawings.ts +5 -1
- package/state/index.ts +1 -56
- package/state/shared.ts +0 -7
- package/state/snapshot.ts +1 -1
- package/state/storage.ts +97 -0
- package/styles/index.css +11 -1
- package/uno.config.ts +1 -0
- package/logic/hmr.ts +0 -3
package/builtin/VSwitch.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { recomputeAllPoppers } from 'floating-vue'
|
|
|
3
3
|
import { defineComponent, h, onMounted, onUnmounted, ref, TransitionGroup, watchEffect } from 'vue'
|
|
4
4
|
import { CLASS_VCLICK_CURRENT, CLASS_VCLICK_DISPLAY_NONE, CLASS_VCLICK_PRIOR, CLASS_VCLICK_TARGET, CLICKS_MAX } from '../constants'
|
|
5
5
|
import { useSlideContext } from '../context'
|
|
6
|
-
import { skipTransition } from '../logic/hmr'
|
|
7
6
|
import { resolveTransition } from '../logic/transition'
|
|
8
7
|
import { makeId } from '../logic/utils'
|
|
8
|
+
import { hmrSkipTransition } from '../state'
|
|
9
9
|
|
|
10
10
|
export default defineComponent({
|
|
11
11
|
props: {
|
|
@@ -77,7 +77,7 @@ export default defineComponent({
|
|
|
77
77
|
|
|
78
78
|
function onAfterLeave() {
|
|
79
79
|
// Refer to SlidesShow.vue
|
|
80
|
-
|
|
80
|
+
hmrSkipTransition.value = true
|
|
81
81
|
recomputeAllPoppers()
|
|
82
82
|
}
|
|
83
83
|
const transitionProps = transition && {
|
|
@@ -107,7 +107,7 @@ export default defineComponent({
|
|
|
107
107
|
}, slot?.()))
|
|
108
108
|
}
|
|
109
109
|
return transitionProps
|
|
110
|
-
? h(TransitionGroup,
|
|
110
|
+
? h(TransitionGroup, hmrSkipTransition.value ? {} : transitionProps, () => children)
|
|
111
111
|
: h(tag, children)
|
|
112
112
|
}
|
|
113
113
|
},
|
package/composables/useClicks.ts
CHANGED
|
@@ -165,7 +165,7 @@ export function createFixedClicks(
|
|
|
165
165
|
): ClicksContext {
|
|
166
166
|
const clicksStart = route?.meta.slide?.frontmatter.clicksStart ?? 0
|
|
167
167
|
return createClicksContextBase(
|
|
168
|
-
|
|
168
|
+
ref(Math.max(toValue(currentInit), clicksStart)),
|
|
169
169
|
clicksStart,
|
|
170
170
|
route?.meta?.clicks,
|
|
171
171
|
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Ref } from 'vue'
|
|
2
|
+
import { useEventListener } from '@vueuse/core'
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import { hideCursorIdle } from '../state'
|
|
5
|
+
|
|
6
|
+
const TIMEOUT = 2000
|
|
7
|
+
|
|
8
|
+
export function useHideCursorIdle(
|
|
9
|
+
enabled: Ref<boolean>,
|
|
10
|
+
) {
|
|
11
|
+
const shouldHide = computed(() => enabled.value && hideCursorIdle.value)
|
|
12
|
+
|
|
13
|
+
function hide() {
|
|
14
|
+
document.body.style.cursor = 'none'
|
|
15
|
+
}
|
|
16
|
+
function show() {
|
|
17
|
+
document.body.style.cursor = ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let timer: ReturnType<typeof setTimeout> | null = null
|
|
21
|
+
|
|
22
|
+
useEventListener(
|
|
23
|
+
document.body,
|
|
24
|
+
['pointermove', 'pointerdown'],
|
|
25
|
+
() => {
|
|
26
|
+
show()
|
|
27
|
+
if (!shouldHide.value)
|
|
28
|
+
return
|
|
29
|
+
if (timer)
|
|
30
|
+
clearTimeout(timer)
|
|
31
|
+
timer = setTimeout(hide, TIMEOUT)
|
|
32
|
+
},
|
|
33
|
+
{ passive: true },
|
|
34
|
+
)
|
|
35
|
+
}
|
package/composables/useNav.ts
CHANGED
|
@@ -9,10 +9,10 @@ import { computed, ref, watch } from 'vue'
|
|
|
9
9
|
import { useRoute, useRouter } from 'vue-router'
|
|
10
10
|
import { CLICKS_MAX } from '../constants'
|
|
11
11
|
import { configs } from '../env'
|
|
12
|
-
import { skipTransition } from '../logic/hmr'
|
|
13
12
|
import { useRouteQuery } from '../logic/route'
|
|
14
13
|
import { getSlide, getSlidePath } from '../logic/slides'
|
|
15
14
|
import { getCurrentTransition } from '../logic/transition'
|
|
15
|
+
import { hmrSkipTransition } from '../state'
|
|
16
16
|
import { createClicksContextBase } from './useClicks'
|
|
17
17
|
import { useTocTree } from './useTocTree'
|
|
18
18
|
|
|
@@ -184,7 +184,7 @@ export function useNavBase(
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
async function go(no: number | string, clicks: number = 0, force = false) {
|
|
187
|
-
|
|
187
|
+
hmrSkipTransition.value = false
|
|
188
188
|
const pageChanged = currentSlideNo.value !== no
|
|
189
189
|
const clicksChanged = clicks !== queryClicks.value
|
|
190
190
|
const meta = getSlide(no)?.meta
|
|
@@ -304,7 +304,7 @@ const useNavState = createSharedComposable((): SlidevContextNavState => {
|
|
|
304
304
|
return v
|
|
305
305
|
},
|
|
306
306
|
set(v) {
|
|
307
|
-
|
|
307
|
+
hmrSkipTransition.value = false
|
|
308
308
|
queryClicksRaw.value = v.toString()
|
|
309
309
|
},
|
|
310
310
|
})
|
package/composables/useTimer.ts
CHANGED
|
@@ -5,10 +5,14 @@ import { wakeLockEnabled } from '../state'
|
|
|
5
5
|
export function useWakeLock() {
|
|
6
6
|
const { request, release } = useVueUseWakeLock()
|
|
7
7
|
|
|
8
|
-
watch(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
watch(
|
|
9
|
+
wakeLockEnabled,
|
|
10
|
+
(enabled) => {
|
|
11
|
+
if (enabled)
|
|
12
|
+
request('screen')
|
|
13
|
+
else
|
|
14
|
+
release()
|
|
15
|
+
},
|
|
16
|
+
{ immediate: true },
|
|
17
|
+
)
|
|
14
18
|
}
|
package/constants.ts
CHANGED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import {
|
|
4
|
+
getHashColorFromString,
|
|
5
|
+
getHsla,
|
|
6
|
+
} from '../logic/color'
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
text?: string
|
|
11
|
+
color?: boolean | number
|
|
12
|
+
as?: string
|
|
13
|
+
size?: string
|
|
14
|
+
}>(),
|
|
15
|
+
{
|
|
16
|
+
color: true,
|
|
17
|
+
},
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const style = computed(() => {
|
|
21
|
+
if (!props.text || props.color === false)
|
|
22
|
+
return {}
|
|
23
|
+
return {
|
|
24
|
+
color: typeof props.color === 'number'
|
|
25
|
+
? getHsla(props.color)
|
|
26
|
+
: getHashColorFromString(props.text),
|
|
27
|
+
background: typeof props.color === 'number'
|
|
28
|
+
? getHsla(props.color, 0.1)
|
|
29
|
+
: getHashColorFromString(props.text, 0.1),
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const sizeClasses = computed(() => {
|
|
34
|
+
switch (props.size || 'sm') {
|
|
35
|
+
case 'sm':
|
|
36
|
+
return 'px-1.5 text-11px leading-1.6em'
|
|
37
|
+
}
|
|
38
|
+
return ''
|
|
39
|
+
})
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<template>
|
|
43
|
+
<component :is="as || 'span'" ws-nowrap rounded :class="sizeClasses" :style>
|
|
44
|
+
<slot>
|
|
45
|
+
<span v-text="props.text" />
|
|
46
|
+
</slot>
|
|
47
|
+
</component>
|
|
48
|
+
</template>
|
|
@@ -43,13 +43,23 @@ ensureDevicesListPermissions()
|
|
|
43
43
|
</script>
|
|
44
44
|
|
|
45
45
|
<template>
|
|
46
|
-
<div
|
|
47
|
-
<SelectList
|
|
48
|
-
|
|
46
|
+
<div text-sm flex="~ col gap-2">
|
|
47
|
+
<SelectList
|
|
48
|
+
v-model="currentCamera"
|
|
49
|
+
title="Camera"
|
|
50
|
+
:items="camerasItems"
|
|
51
|
+
/>
|
|
52
|
+
<div class="h-1px opacity-10 bg-current w-full" />
|
|
53
|
+
<SelectList
|
|
54
|
+
v-model="currentMic"
|
|
55
|
+
title="Microphone"
|
|
56
|
+
:items="microphonesItems"
|
|
57
|
+
/>
|
|
58
|
+
<div class="h-1px opacity-10 bg-current w-full" />
|
|
49
59
|
<SelectList
|
|
50
60
|
v-if="mimeTypeItems.length"
|
|
51
61
|
v-model="mimeType"
|
|
52
|
-
title="
|
|
62
|
+
title="Video Format"
|
|
53
63
|
:items="mimeTypeItems"
|
|
54
64
|
/>
|
|
55
65
|
</div>
|
package/internals/FormItem.vue
CHANGED
|
@@ -6,6 +6,7 @@ defineProps<{
|
|
|
6
6
|
nested?: boolean | number
|
|
7
7
|
div?: boolean
|
|
8
8
|
description?: string
|
|
9
|
+
dot?: boolean
|
|
9
10
|
}>()
|
|
10
11
|
|
|
11
12
|
const emit = defineEmits<{
|
|
@@ -19,17 +20,19 @@ function reset() {
|
|
|
19
20
|
|
|
20
21
|
<template>
|
|
21
22
|
<component :is="div ? 'div' : 'label'" flex="~ row gap-2 items-center" select-none>
|
|
22
|
-
<div w-30 h-
|
|
23
|
+
<div w-30 h-8 flex="~ gap-1 items-center">
|
|
23
24
|
<div
|
|
24
25
|
v-if="nested" i-ri-corner-down-right-line op40
|
|
25
26
|
:style="typeof nested === 'number' ? { marginLeft: `${nested * 0.5 + 0.5}rem` } : { marginLeft: '0.25rem' }"
|
|
26
27
|
/>
|
|
27
|
-
<div v-if="!description" op75 @dblclick="reset">
|
|
28
|
+
<div v-if="!description" op75 relative @dblclick="reset">
|
|
28
29
|
{{ title }}
|
|
30
|
+
<div v-if="dot" w-1.5 h-1.5 bg-primary rounded absolute top-0 right--2 />
|
|
29
31
|
</div>
|
|
30
32
|
<Tooltip v-else distance="10">
|
|
31
|
-
<div op75 text-right @dblclick="reset">
|
|
33
|
+
<div op75 text-right relative @dblclick="reset">
|
|
32
34
|
{{ title }}
|
|
35
|
+
<div v-if="dot" w-1.5 h-1.5 bg-primary rounded absolute top-0 right--2 />
|
|
33
36
|
</div>
|
|
34
37
|
<template #popper>
|
|
35
38
|
<div text-sm min-w-90 v-html="description" />
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
const props = defineProps<{
|
|
3
|
+
max: number
|
|
4
|
+
min: number
|
|
5
|
+
step: number
|
|
6
|
+
unit?: string
|
|
7
|
+
default?: number
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const value = defineModel<number>('modelValue', {
|
|
11
|
+
type: Number,
|
|
12
|
+
})
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<template>
|
|
16
|
+
<div relative h-22px w-60 flex-auto @dblclick="props.default !== undefined ? value = props.default : null">
|
|
17
|
+
<input
|
|
18
|
+
v-model.number="value" type="range" class="slider"
|
|
19
|
+
v-bind="props"
|
|
20
|
+
absolute bottom-0 left-0 right-0 top-0 z-10 w-full align-top
|
|
21
|
+
>
|
|
22
|
+
<span
|
|
23
|
+
v-if="props.default != null"
|
|
24
|
+
border="r main" absolute bottom-0 top-0 h-full w-1px op75
|
|
25
|
+
:style="{
|
|
26
|
+
left: `${(props.default - min) / (max - min) * 100}%`,
|
|
27
|
+
}"
|
|
28
|
+
/>
|
|
29
|
+
</div>
|
|
30
|
+
<div relative h-22px>
|
|
31
|
+
<input v-model.number="value" type="number" v-bind="props" border="~ base rounded" m0 w-20 bg-gray:5 pl2 align-top text-sm>
|
|
32
|
+
<span v-if="props.unit" pointer-events-none absolute right-1 top-0.5 text-xs op25>{{ props.unit }}</span>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<style>
|
|
37
|
+
.slider {
|
|
38
|
+
appearance: none;
|
|
39
|
+
height: 22px;
|
|
40
|
+
outline: none;
|
|
41
|
+
opacity: 0.7;
|
|
42
|
+
-webkit-transition: 0.2s;
|
|
43
|
+
transition: opacity 0.2s;
|
|
44
|
+
--uno: border border-main rounded of-hidden bg-gray/5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.slider:hover {
|
|
48
|
+
opacity: 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.slider::-webkit-slider-thumb {
|
|
52
|
+
-webkit-appearance: none;
|
|
53
|
+
appearance: none;
|
|
54
|
+
width: 5px;
|
|
55
|
+
height: 22px;
|
|
56
|
+
background: var(--slidev-theme-primary);
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
z-index: 10;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.slider::-moz-range-thumb {
|
|
62
|
+
width: 5px;
|
|
63
|
+
height: 22px;
|
|
64
|
+
background: var(--slidev-theme-primary);
|
|
65
|
+
cursor: pointer;
|
|
66
|
+
z-index: 10;
|
|
67
|
+
}
|
|
68
|
+
</style>
|
package/internals/MenuButton.vue
CHANGED
|
@@ -30,8 +30,8 @@ onClickOutside(el, () => {
|
|
|
30
30
|
<KeepAlive>
|
|
31
31
|
<div
|
|
32
32
|
v-if="value"
|
|
33
|
-
class="
|
|
34
|
-
|
|
33
|
+
class="bg-main text-main shadow-xl absolute bottom-10 left-0 z-menu py2"
|
|
34
|
+
border="~ main rounded-md"
|
|
35
35
|
>
|
|
36
36
|
<slot name="menu" />
|
|
37
37
|
</div>
|
|
@@ -5,11 +5,12 @@ import { useDrawings } from '../composables/useDrawings'
|
|
|
5
5
|
import { useNav } from '../composables/useNav'
|
|
6
6
|
import { configs } from '../env'
|
|
7
7
|
import { isColorSchemaConfigured, isDark, toggleDark } from '../logic/dark'
|
|
8
|
-
import { activeElement, breakpoints, fullscreen, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
8
|
+
import { activeElement, breakpoints, fullscreen, hasViewerCssFilter, presenterLayout, showEditor, showInfoDialog, showPresenterCursor, toggleOverview, togglePresenterLayout } from '../state'
|
|
9
9
|
import { downloadPDF } from '../utils'
|
|
10
10
|
import IconButton from './IconButton.vue'
|
|
11
11
|
import MenuButton from './MenuButton.vue'
|
|
12
12
|
import Settings from './Settings.vue'
|
|
13
|
+
import SyncControls from './SyncControls.vue'
|
|
13
14
|
|
|
14
15
|
import VerticalDivider from './VerticalDivider.vue'
|
|
15
16
|
|
|
@@ -48,7 +49,7 @@ function onMouseLeave() {
|
|
|
48
49
|
|
|
49
50
|
const barStyle = computed(() => props.persist
|
|
50
51
|
? 'text-$slidev-controls-foreground bg-transparent'
|
|
51
|
-
: 'rounded-md bg-main shadow-xl
|
|
52
|
+
: 'rounded-md bg-main shadow-xl border border-main')
|
|
52
53
|
|
|
53
54
|
const RecordingControls = shallowRef<any>()
|
|
54
55
|
if (__SLIDEV_FEATURE_RECORD__)
|
|
@@ -130,19 +131,15 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
130
131
|
>
|
|
131
132
|
<div class="i-carbon:text-annotation-toggle" />
|
|
132
133
|
</IconButton>
|
|
133
|
-
|
|
134
|
-
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial flex items-center" @click="togglePresenterLayout">
|
|
135
|
-
<div class="i-carbon:template" />
|
|
136
|
-
{{ presenterLayout }}
|
|
137
|
-
</IconButton>
|
|
138
134
|
</template>
|
|
135
|
+
|
|
139
136
|
<template v-if="!__DEV__">
|
|
140
137
|
<IconButton v-if="configs.download" title="Download as PDF" @click="downloadPDF">
|
|
141
138
|
<div class="i-carbon:download" />
|
|
142
139
|
</IconButton>
|
|
143
140
|
</template>
|
|
144
141
|
|
|
145
|
-
<template v-if="__SLIDEV_FEATURE_BROWSER_EXPORTER__">
|
|
142
|
+
<template v-if="__SLIDEV_FEATURE_BROWSER_EXPORTER__ && !isEmbedded && !isPresenter">
|
|
146
143
|
<IconButton title="Browser Exporter" to="/export">
|
|
147
144
|
<div class="i-carbon:document-pdf" />
|
|
148
145
|
</IconButton>
|
|
@@ -156,11 +153,21 @@ if (__SLIDEV_FEATURE_RECORD__)
|
|
|
156
153
|
<div class="i-carbon:information" />
|
|
157
154
|
</IconButton>
|
|
158
155
|
|
|
159
|
-
<template v-if="!
|
|
156
|
+
<template v-if="!isEmbedded">
|
|
157
|
+
<VerticalDivider />
|
|
158
|
+
|
|
159
|
+
<IconButton v-if="isPresenter" title="Toggle Presenter Layout" class="aspect-ratio-initial flex items-center" @click="togglePresenterLayout">
|
|
160
|
+
<div class="i-carbon:template" />
|
|
161
|
+
{{ presenterLayout }}
|
|
162
|
+
</IconButton>
|
|
163
|
+
|
|
164
|
+
<SyncControls v-if="__SLIDEV_FEATURE_PRESENTER__" />
|
|
165
|
+
|
|
160
166
|
<MenuButton>
|
|
161
167
|
<template #button>
|
|
162
168
|
<IconButton title="More Options">
|
|
163
169
|
<div class="i-carbon:settings-adjust" />
|
|
170
|
+
<div v-if="hasViewerCssFilter" w-2 h-2 bg-primary rounded-full absolute top-0.5 right-0.5 />
|
|
164
171
|
</IconButton>
|
|
165
172
|
</template>
|
|
166
173
|
<template #menu>
|
|
@@ -4,15 +4,18 @@ import { computed, ref, watchEffect } from 'vue'
|
|
|
4
4
|
import { createFixedClicks } from '../composables/useClicks'
|
|
5
5
|
import { useNav } from '../composables/useNav'
|
|
6
6
|
import { CLICKS_MAX } from '../constants'
|
|
7
|
-
import {
|
|
7
|
+
import { pathPrefix } from '../env'
|
|
8
8
|
import { currentOverviewPage, overviewRowCount } from '../logic/overview'
|
|
9
|
+
import { isScreenshotSupported } from '../logic/screenshot'
|
|
10
|
+
import { snapshotManager } from '../logic/snapshot'
|
|
9
11
|
import { breakpoints, showOverview, windowSize } from '../state'
|
|
10
12
|
import DrawingPreview from './DrawingPreview.vue'
|
|
11
13
|
import IconButton from './IconButton.vue'
|
|
12
14
|
import SlideContainer from './SlideContainer.vue'
|
|
13
15
|
import SlideWrapper from './SlideWrapper.vue'
|
|
14
16
|
|
|
15
|
-
const
|
|
17
|
+
const nav = useNav()
|
|
18
|
+
const { currentSlideNo, go: goSlide, slides } = nav
|
|
16
19
|
|
|
17
20
|
function close() {
|
|
18
21
|
showOverview.value = false
|
|
@@ -48,6 +51,12 @@ const rowCount = computed(() => {
|
|
|
48
51
|
|
|
49
52
|
const keyboardBuffer = ref<string>('')
|
|
50
53
|
|
|
54
|
+
async function captureSlidesOverview() {
|
|
55
|
+
showOverview.value = false
|
|
56
|
+
await snapshotManager.startCapturing(nav)
|
|
57
|
+
showOverview.value = true
|
|
58
|
+
}
|
|
59
|
+
|
|
51
60
|
useEventListener('keypress', (e) => {
|
|
52
61
|
if (!showOverview.value) {
|
|
53
62
|
keyboardBuffer.value = ''
|
|
@@ -129,7 +138,7 @@ watchEffect(() => {
|
|
|
129
138
|
<SlideContainer
|
|
130
139
|
:key="route.no"
|
|
131
140
|
:no="route.no"
|
|
132
|
-
:use-snapshot="
|
|
141
|
+
:use-snapshot="true"
|
|
133
142
|
:width="cardWidth"
|
|
134
143
|
class="pointer-events-none"
|
|
135
144
|
>
|
|
@@ -157,7 +166,10 @@ watchEffect(() => {
|
|
|
157
166
|
</div>
|
|
158
167
|
</div>
|
|
159
168
|
</Transition>
|
|
160
|
-
<div
|
|
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
|
+
>
|
|
161
173
|
<IconButton title="Close" class="text-2xl" @click="close">
|
|
162
174
|
<div class="i-carbon:close" />
|
|
163
175
|
</IconButton>
|
|
@@ -172,5 +184,13 @@ watchEffect(() => {
|
|
|
172
184
|
>
|
|
173
185
|
<div class="i-carbon:list-boxes" />
|
|
174
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>
|
|
175
195
|
</div>
|
|
176
196
|
</template>
|
|
@@ -3,7 +3,7 @@ import { useLocalStorage } from '@vueuse/core'
|
|
|
3
3
|
import { onMounted, watch } from 'vue'
|
|
4
4
|
import { recorder } from '../logic/recording'
|
|
5
5
|
import { currentCamera, showRecordingDialog } from '../state'
|
|
6
|
-
import
|
|
6
|
+
import DevicesSelectors from './DevicesSelectors.vue'
|
|
7
7
|
import IconButton from './IconButton.vue'
|
|
8
8
|
import MenuButton from './MenuButton.vue'
|
|
9
9
|
|
|
@@ -59,7 +59,7 @@ onMounted(() => {
|
|
|
59
59
|
</IconButton>
|
|
60
60
|
</template>
|
|
61
61
|
<template #menu>
|
|
62
|
-
<
|
|
62
|
+
<DevicesSelectors />
|
|
63
63
|
</template>
|
|
64
64
|
</MenuButton>
|
|
65
65
|
</template>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { useVModel } from '@vueuse/core'
|
|
3
3
|
import { nextTick } from 'vue'
|
|
4
4
|
import { getFilename, mimeType, recordCamera, recorder, recordingName } from '../logic/recording'
|
|
5
|
-
import
|
|
5
|
+
import DevicesSelectors from './DevicesSelectors.vue'
|
|
6
6
|
import Modal from './Modal.vue'
|
|
7
7
|
|
|
8
8
|
const props = defineProps({
|
|
@@ -72,14 +72,14 @@ async function start() {
|
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
74
74
|
</div>
|
|
75
|
-
<
|
|
75
|
+
<DevicesSelectors />
|
|
76
76
|
</div>
|
|
77
77
|
<div class="flex my-1">
|
|
78
|
-
<button class="
|
|
78
|
+
<button class="slidev-form-button" @click="close">
|
|
79
79
|
Cancel
|
|
80
80
|
</button>
|
|
81
81
|
<div class="flex-auto" />
|
|
82
|
-
<button @click="start">
|
|
82
|
+
<button class="slidev-form-button primary" @click="start">
|
|
83
83
|
Start
|
|
84
84
|
</button>
|
|
85
85
|
</div>
|
|
@@ -111,15 +111,5 @@ async function start() {
|
|
|
111
111
|
input[type='text'] {
|
|
112
112
|
@apply border border-main rounded px-2 py-1;
|
|
113
113
|
}
|
|
114
|
-
|
|
115
|
-
button {
|
|
116
|
-
@apply bg-orange-400 text-white px-4 py-1 rounded border-b-2 border-orange-600;
|
|
117
|
-
@apply hover:(bg-orange-500 border-orange-700);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
button.cancel {
|
|
121
|
-
@apply bg-gray-400 bg-opacity-50 text-white px-4 py-1 rounded border-b-2 border-main;
|
|
122
|
-
@apply hover:(bg-opacity-75 border-opacity-75);
|
|
123
|
-
}
|
|
124
114
|
}
|
|
125
115
|
</style>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { shallowRef, useTemplateRef } from 'vue'
|
|
3
|
+
|
|
4
|
+
const video = useTemplateRef('video')
|
|
5
|
+
const stream = shallowRef<MediaStream | null>(null)
|
|
6
|
+
const started = shallowRef(false)
|
|
7
|
+
|
|
8
|
+
async function startCapture() {
|
|
9
|
+
stream.value = await navigator.mediaDevices.getDisplayMedia({
|
|
10
|
+
video: {
|
|
11
|
+
// @ts-expect-error missing types
|
|
12
|
+
cursor: 'always',
|
|
13
|
+
},
|
|
14
|
+
audio: false,
|
|
15
|
+
selfBrowserSurface: 'include',
|
|
16
|
+
preferCurrentTab: false,
|
|
17
|
+
})
|
|
18
|
+
video.value!.srcObject = stream.value
|
|
19
|
+
video.value!.play()
|
|
20
|
+
started.value = true
|
|
21
|
+
stream.value.addEventListener('inactive', () => {
|
|
22
|
+
video.value!.srcObject = null
|
|
23
|
+
started.value = false
|
|
24
|
+
})
|
|
25
|
+
stream.value.addEventListener('ended', () => {
|
|
26
|
+
video.value!.srcObject = null
|
|
27
|
+
started.value = false
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<template>
|
|
33
|
+
<div h-full w-full>
|
|
34
|
+
<video v-show="started" ref="video" class="w-full h-full object-contain" />
|
|
35
|
+
<div v-if="!started" w-full h-full flex="~ col gap-4 items-center justify-center">
|
|
36
|
+
<div op50>
|
|
37
|
+
Use screen capturing to mirror your main screen back to presenter view.<br>
|
|
38
|
+
Click the button below and <b>select your other monitor or window</b>.
|
|
39
|
+
</div>
|
|
40
|
+
<button class="slidev-form-button" @click="startCapture">
|
|
41
|
+
Start Screen Mirroring
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import Badge from './Badge.vue'
|
|
3
|
+
|
|
4
|
+
defineProps<{
|
|
5
|
+
options: { label: string, value: any }[]
|
|
6
|
+
modelValue: any
|
|
7
|
+
}>()
|
|
8
|
+
|
|
9
|
+
defineEmits<{
|
|
10
|
+
(event: 'update:modelValue', newValue: any): void
|
|
11
|
+
}>()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<div flex="~ gap-1 items-center" rounded bg-gray:4 p1 m--1>
|
|
16
|
+
<Badge
|
|
17
|
+
v-for="option in options"
|
|
18
|
+
:key="option.value"
|
|
19
|
+
class="px-2 py-1 text-xs font-mono"
|
|
20
|
+
:class="option.value === modelValue ? '' : 'op50'"
|
|
21
|
+
:color="option.value === modelValue"
|
|
22
|
+
:aria-pressed="option.value === modelValue"
|
|
23
|
+
size="none"
|
|
24
|
+
:text="option.label"
|
|
25
|
+
as="button"
|
|
26
|
+
@click="$emit('update:modelValue', option.value)"
|
|
27
|
+
/>
|
|
28
|
+
</div>
|
|
29
|
+
</template>
|
package/internals/SelectList.vue
CHANGED
|
@@ -44,19 +44,11 @@ const value = useVModel(props, 'modelValue', emit, { passive: true })
|
|
|
44
44
|
</template>
|
|
45
45
|
|
|
46
46
|
<style lang="postcss" scoped>
|
|
47
|
-
.select-list {
|
|
48
|
-
@apply my-2;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
47
|
.item {
|
|
52
48
|
@apply flex rounded whitespace-nowrap py-1 gap-1 px-2 cursor-default hover:bg-gray-400 hover:bg-opacity-10;
|
|
53
|
-
|
|
54
|
-
svg {
|
|
55
|
-
@apply mr-1 -ml-2 my-auto;
|
|
56
|
-
}
|
|
57
49
|
}
|
|
58
50
|
|
|
59
51
|
.title {
|
|
60
|
-
@apply text-
|
|
52
|
+
@apply text-sm op75 px3 py1 select-none text-nowrap font-bold;
|
|
61
53
|
}
|
|
62
54
|
</style>
|