102 lines
2.5 KiB
Vue
102 lines
2.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
import { useNav } from '@slidev/client'
|
|
|
|
const { isPresenter, currentSlideNo, nextSlide } = useNav()
|
|
|
|
const LIMIT = 20
|
|
const elapsed = ref(0)
|
|
const hasAdvanced = ref(false)
|
|
const paused = ref(true)
|
|
let timer: ReturnType<typeof setInterval> | null = null
|
|
|
|
const togglePause = () => {
|
|
paused.value = !paused.value
|
|
}
|
|
|
|
const advanceSlide = () => {
|
|
if (hasAdvanced.value || elapsed.value < LIMIT)
|
|
return
|
|
|
|
hasAdvanced.value = true
|
|
nextSlide()
|
|
}
|
|
|
|
onMounted(() => {
|
|
timer = setInterval(() => {
|
|
if (paused.value)
|
|
return
|
|
elapsed.value++
|
|
advanceSlide()
|
|
}, 1000)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (timer)
|
|
clearInterval(timer)
|
|
timer = null
|
|
})
|
|
|
|
watch(currentSlideNo, () => {
|
|
elapsed.value = 0
|
|
hasAdvanced.value = false
|
|
paused.value = false
|
|
})
|
|
|
|
const remaining = computed(() => LIMIT - elapsed.value)
|
|
const over = computed(() => remaining.value < 0)
|
|
const warn = computed(() => !over.value && remaining.value <= 5)
|
|
const pct = computed(() => Math.min(100, (elapsed.value / LIMIT) * 100))
|
|
const label = computed(() => `${over.value ? '+' : ''}${Math.abs(remaining.value)}s`)
|
|
</script>
|
|
|
|
<template>
|
|
<div v-if="isPresenter" class="pk-timer" :class="{ over, warn, paused }" @click="togglePause">
|
|
<div class="pk-bar" :style="{ width: pct + '%' }" />
|
|
<span class="pk-label">{{ paused ? '⏸' : label }}</span>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pk-timer {
|
|
position: fixed;
|
|
top: 12px;
|
|
right: 12px;
|
|
z-index: 9999;
|
|
width: 130px;
|
|
height: 38px;
|
|
border-radius: 10px;
|
|
background: rgba(20, 20, 25, 0.85);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: 'Fira Code', monospace;
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.pk-timer.paused { border-color: rgba(96, 165, 250, 0.8); }
|
|
.pk-timer.paused .pk-bar { background: rgba(96, 165, 250, 0.35); }
|
|
.pk-bar {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
background: rgba(74, 222, 128, 0.35);
|
|
transition: width 1s linear;
|
|
}
|
|
.pk-timer.warn .pk-bar { background: rgba(251, 191, 36, 0.4); }
|
|
.pk-timer.over { border-color: rgba(248, 113, 113, 0.8); }
|
|
.pk-timer.over .pk-bar { width: 100% !important; background: rgba(248, 113, 113, 0.45); }
|
|
.pk-label {
|
|
position: relative;
|
|
font-size: 20px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.pk-timer.over .pk-label { color: #fecaca; }
|
|
</style>
|