Stop clamping extensions to 3s or 5s so each segment can use the same length control as generate.
111 lines
3.4 KiB
Vue
111 lines
3.4 KiB
Vue
<template>
|
|
<div class="text-sm">
|
|
<span v-if="label" class="mb-1 block text-zinc-400">{{ label }}</span>
|
|
<div class="relative">
|
|
<input
|
|
v-model="draft"
|
|
type="text"
|
|
inputmode="decimal"
|
|
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 pr-8 cursor-ns-resize outline-none ring-amber-300/40 placeholder:text-zinc-600 focus:cursor-text focus:ring-2"
|
|
:class="{ 'select-none caret-transparent': scrubbing }"
|
|
placeholder="seconds"
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
@blur="commit"
|
|
@keydown.enter.prevent="commit"
|
|
@pointerdown="onPointerDown"
|
|
>
|
|
<span class="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-zinc-500">s</span>
|
|
</div>
|
|
<input
|
|
class="aigen-slider mt-3 w-full"
|
|
type="range"
|
|
:min="SLIDER_MIN"
|
|
:max="SLIDER_MAX"
|
|
:step="0.1"
|
|
:value="slider"
|
|
:style="{ background: `linear-gradient(to right, #fbbf24 ${sliderPct}%, #27272a ${sliderPct}%)` }"
|
|
:aria-label="label || 'Duration in seconds'"
|
|
@input="onSlider"
|
|
>
|
|
<p v-if="hint" class="mt-1 text-[11px] text-zinc-500">{{ hint }}</p>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const MIN = 0.5
|
|
const MAX = 120
|
|
const SLIDER_MIN = 1
|
|
const SLIDER_MAX = 30
|
|
|
|
withDefaults(defineProps<{
|
|
label?: string
|
|
hint?: string
|
|
}>(), {
|
|
label: '',
|
|
hint: ''
|
|
})
|
|
|
|
const model = defineModel<number>({ required: true })
|
|
const draft = ref(formatDuration(model.value))
|
|
const scrubbing = ref(false)
|
|
|
|
const slider = computed(() => Math.min(SLIDER_MAX, Math.max(SLIDER_MIN, model.value)))
|
|
const sliderPct = computed(() => ((slider.value - SLIDER_MIN) / (SLIDER_MAX - SLIDER_MIN)) * 100)
|
|
|
|
watch(model, (value) => {
|
|
if (!scrubbing.value) draft.value = formatDuration(value)
|
|
})
|
|
|
|
function clampDuration(value: number) {
|
|
if (!Number.isFinite(value)) return 5
|
|
return Math.min(MAX, Math.max(MIN, Math.round(value * 10) / 10))
|
|
}
|
|
|
|
function formatDuration(value: number) {
|
|
return Number.isInteger(value) ? String(value) : value.toFixed(1).replace(/\.0$/, '')
|
|
}
|
|
|
|
function commit() {
|
|
const parsed = Number.parseFloat(draft.value.replace(',', '.').trim())
|
|
model.value = clampDuration(parsed)
|
|
draft.value = formatDuration(model.value)
|
|
}
|
|
|
|
function onSlider(event: Event) {
|
|
model.value = clampDuration(Number((event.target as HTMLInputElement).value))
|
|
draft.value = formatDuration(model.value)
|
|
}
|
|
|
|
function onPointerDown(event: PointerEvent) {
|
|
if (event.button !== 0) return
|
|
const input = event.currentTarget as HTMLInputElement
|
|
const startY = event.clientY
|
|
const startValue = model.value
|
|
let moved = false
|
|
const onMove = (move: PointerEvent) => {
|
|
const delta = startY - move.clientY
|
|
if (!moved && Math.abs(delta) < 5) return
|
|
if (!moved) {
|
|
moved = true
|
|
scrubbing.value = true
|
|
input.setPointerCapture(move.pointerId)
|
|
input.blur()
|
|
}
|
|
move.preventDefault()
|
|
const next = clampDuration(startValue + delta / 18)
|
|
model.value = next
|
|
draft.value = formatDuration(next)
|
|
}
|
|
const onUp = () => {
|
|
input.removeEventListener('pointermove', onMove)
|
|
input.removeEventListener('pointerup', onUp)
|
|
input.removeEventListener('pointercancel', onUp)
|
|
scrubbing.value = false
|
|
}
|
|
input.addEventListener('pointermove', onMove)
|
|
input.addEventListener('pointerup', onUp)
|
|
input.addEventListener('pointercancel', onUp)
|
|
}
|
|
</script>
|