build: add precompiled CLI distribution
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||
|
||||
const configuredHash = import.meta.env.VITE_EASY_SLIDES_PRESENTER_HASH || ''
|
||||
const isPresenter = computed(() => /\/presenter(?:\/\d+)?\/?$/.test(window.location.pathname))
|
||||
const storageKey = `easy-slides:presenter:${import.meta.env.BASE_URL}`
|
||||
const unlocked = ref(false)
|
||||
const token = ref('')
|
||||
const error = ref('')
|
||||
const noteSize = ref(20)
|
||||
const contrast = ref(false)
|
||||
const gate = ref<HTMLDialogElement>()
|
||||
const tokenInput = ref<HTMLInputElement>()
|
||||
|
||||
onMounted(async () => {
|
||||
unlocked.value = sessionStorage.getItem(storageKey) === configuredHash
|
||||
|| (!configuredHash && import.meta.env.DEV)
|
||||
syncBodyState()
|
||||
await syncPresenterGate()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (gate.value?.open)
|
||||
gate.value.close()
|
||||
document.body.classList.remove('easy-presenter-active', 'easy-presenter-locked', 'easy-notes-contrast')
|
||||
})
|
||||
|
||||
watch(unlocked, () => void syncPresenterGate())
|
||||
|
||||
async function unlock() {
|
||||
error.value = ''
|
||||
const candidate = await sha256(token.value)
|
||||
if (candidate !== configuredHash) {
|
||||
error.value = 'Token 不正确'
|
||||
return
|
||||
}
|
||||
sessionStorage.setItem(storageKey, configuredHash)
|
||||
unlocked.value = true
|
||||
token.value = ''
|
||||
await nextTick()
|
||||
syncBodyState()
|
||||
}
|
||||
|
||||
function lock() {
|
||||
sessionStorage.removeItem(storageKey)
|
||||
unlocked.value = false
|
||||
syncBodyState()
|
||||
}
|
||||
|
||||
function openPresenter() {
|
||||
window.location.href = `${import.meta.env.BASE_URL}presenter/`
|
||||
}
|
||||
|
||||
function resizeNotes(delta: number) {
|
||||
noteSize.value = Math.min(38, Math.max(14, noteSize.value + delta))
|
||||
document.documentElement.style.setProperty('--easy-presenter-note-size', `${noteSize.value}px`)
|
||||
}
|
||||
|
||||
function toggleContrast() {
|
||||
contrast.value = !contrast.value
|
||||
syncBodyState()
|
||||
}
|
||||
|
||||
function syncBodyState() {
|
||||
document.body.classList.toggle('easy-presenter-active', isPresenter.value && unlocked.value)
|
||||
document.body.classList.toggle('easy-notes-contrast', isPresenter.value && unlocked.value && contrast.value)
|
||||
}
|
||||
|
||||
async function syncPresenterGate() {
|
||||
await nextTick()
|
||||
const shouldOpen = isPresenter.value && !unlocked.value
|
||||
document.body.classList.toggle('easy-presenter-locked', shouldOpen)
|
||||
if (!shouldOpen) {
|
||||
if (gate.value?.open)
|
||||
gate.value.close()
|
||||
return
|
||||
}
|
||||
if (gate.value && !gate.value.open)
|
||||
gate.value.showModal()
|
||||
tokenInput.value?.focus()
|
||||
}
|
||||
|
||||
function keepGateOpen(event: Event) {
|
||||
event.preventDefault()
|
||||
tokenInput.value?.focus()
|
||||
}
|
||||
|
||||
async function sha256(value: string) {
|
||||
const bytes = new TextEncoder().encode(value.trim())
|
||||
const digest = await crypto.subtle.digest('SHA-256', bytes)
|
||||
return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
v-if="!isPresenter && configuredHash"
|
||||
class="easy-presenter-launcher"
|
||||
type="button"
|
||||
title="打开演示者模式"
|
||||
@click="openPresenter"
|
||||
>
|
||||
演示者
|
||||
</button>
|
||||
|
||||
<Teleport to="body">
|
||||
<dialog
|
||||
v-if="isPresenter && !unlocked"
|
||||
ref="gate"
|
||||
class="easy-presenter-gate"
|
||||
aria-labelledby="easy-presenter-title"
|
||||
@cancel="keepGateOpen"
|
||||
@click.stop
|
||||
@keydown.stop
|
||||
@keyup.stop
|
||||
@pointerdown.stop
|
||||
>
|
||||
<section class="easy-presenter-dialog">
|
||||
<h1 id="easy-presenter-title">演示者模式</h1>
|
||||
<p v-if="configuredHash">输入 token 后查看下一页、演讲备注和计时器。</p>
|
||||
<p v-else>该站点没有配置演示者 token。</p>
|
||||
<form v-if="configuredHash" @submit.prevent="unlock">
|
||||
<input ref="tokenInput" v-model="token" type="password" autocomplete="current-password" autofocus aria-label="演示者 token">
|
||||
<button type="submit">解锁</button>
|
||||
</form>
|
||||
<p v-if="error" class="easy-presenter-error" role="alert">{{ error }}</p>
|
||||
</section>
|
||||
</dialog>
|
||||
</Teleport>
|
||||
|
||||
<nav v-if="isPresenter && unlocked" class="easy-presenter-tools" aria-label="提词器设置">
|
||||
<button type="button" title="减小讲稿字号" @click="resizeNotes(-2)">A−</button>
|
||||
<button type="button" title="增大讲稿字号" @click="resizeNotes(2)">A+</button>
|
||||
<button type="button" title="切换高对比讲稿" @click="toggleContrast">◐</button>
|
||||
<button type="button" title="锁定演示者模式" @click="lock">锁定</button>
|
||||
</nav>
|
||||
</template>
|
||||
Reference in New Issue
Block a user