Drop the extra duration text box and slow mobile thumbwheels.
Duration now matches CFG as one wheel; tap the digits to type a length. Touch drags move at a quarter of the desktop rate. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,33 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-sm">
|
<div>
|
||||||
<span v-if="label" class="mb-1 block text-zinc-400">{{ label }}</span>
|
<Thumbwheel
|
||||||
<div class="relative">
|
v-model="model"
|
||||||
<input
|
:label="label || 'Duration'"
|
||||||
v-model="draft"
|
lock-key="duration"
|
||||||
type="text"
|
:min="MIN"
|
||||||
inputmode="decimal"
|
:max="MAX"
|
||||||
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"
|
:step="0.1"
|
||||||
:class="{ 'select-none caret-transparent': scrubbing }"
|
:fine-step="0.1"
|
||||||
placeholder="seconds"
|
:coarse-step="1"
|
||||||
autocomplete="off"
|
:drag-range="29"
|
||||||
spellcheck="false"
|
editable
|
||||||
@blur="commit"
|
unit="s"
|
||||||
@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>
|
|
||||||
<div class="mt-3">
|
|
||||||
<Thumbwheel
|
|
||||||
:model-value="slider"
|
|
||||||
lock-key="duration"
|
|
||||||
:min="SLIDER_MIN"
|
|
||||||
:max="SLIDER_MAX"
|
|
||||||
:step="0.1"
|
|
||||||
compact
|
|
||||||
@update:model-value="onSliderValue"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-if="hint" class="mt-1 text-[11px] text-zinc-500">{{ hint }}</p>
|
<p v-if="hint" class="mt-1 text-[11px] text-zinc-500">{{ hint }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -35,8 +20,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const MIN = 0.5
|
const MIN = 0.5
|
||||||
const MAX = 120
|
const MAX = 120
|
||||||
const SLIDER_MIN = 1
|
|
||||||
const SLIDER_MAX = 30
|
|
||||||
|
|
||||||
withDefaults(defineProps<{
|
withDefaults(defineProps<{
|
||||||
label?: string
|
label?: string
|
||||||
@@ -47,64 +30,4 @@ withDefaults(defineProps<{
|
|||||||
})
|
})
|
||||||
|
|
||||||
const model = defineModel<number>({ required: true })
|
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)))
|
|
||||||
|
|
||||||
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 onSliderValue(value: number) {
|
|
||||||
model.value = clampDuration(value)
|
|
||||||
draft.value = formatDuration(model.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function onPointerDown(event: PointerEvent) {
|
|
||||||
if (event.button !== 0) return
|
|
||||||
if (event.pointerType === 'touch') 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>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-[6.5rem] min-w-0" :class="disabled ? 'opacity-50' : ''">
|
<div class="min-w-0" :class="[disabled ? 'opacity-50' : '', unit ? 'w-[7.25rem]' : 'w-[6.5rem]']">
|
||||||
<div v-if="label" class="mb-1.5 text-[10px] font-medium uppercase tracking-wider text-zinc-500">{{ label }}</div>
|
<div v-if="label" class="mb-1.5 text-[10px] font-medium uppercase tracking-wider text-zinc-500">{{ label }}</div>
|
||||||
<div
|
<div
|
||||||
class="overflow-hidden rounded-2xl border bg-zinc-950"
|
class="overflow-hidden rounded-2xl border bg-zinc-950"
|
||||||
@@ -7,8 +7,36 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-stretch border-b border-white/5">
|
<div class="flex items-stretch border-b border-white/5">
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<SegmentDigits :value="display" />
|
<input
|
||||||
|
v-if="editing"
|
||||||
|
ref="editInput"
|
||||||
|
v-model="draft"
|
||||||
|
type="text"
|
||||||
|
inputmode="decimal"
|
||||||
|
class="h-11 w-full bg-transparent px-2 text-center font-mono text-lg text-amber-300 outline-none"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
@blur="commitEdit"
|
||||||
|
@keydown.enter.prevent="commitEdit"
|
||||||
|
@keydown.escape.prevent="cancelEdit"
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
v-else-if="editable"
|
||||||
|
type="button"
|
||||||
|
class="block w-full cursor-text"
|
||||||
|
:aria-label="`Type ${dialName}`"
|
||||||
|
@click="startEdit"
|
||||||
|
@pointerdown.stop
|
||||||
|
>
|
||||||
|
<SegmentDigits :value="display" />
|
||||||
|
</button>
|
||||||
|
<SegmentDigits v-else :value="display" />
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="unit"
|
||||||
|
class="flex items-end px-1.5 pb-2.5 text-[10px] font-medium uppercase tracking-wider text-zinc-500"
|
||||||
|
>{{ unit }}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex w-7 shrink-0 items-center justify-center border-l transition"
|
class="flex w-7 shrink-0 items-center justify-center border-l transition"
|
||||||
@@ -82,6 +110,9 @@ const props = withDefaults(defineProps<{
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
digits?: number
|
digits?: number
|
||||||
|
editable?: boolean
|
||||||
|
unit?: string
|
||||||
|
dragRange?: number
|
||||||
}>(), {
|
}>(), {
|
||||||
step: 0.1,
|
step: 0.1,
|
||||||
fineStep: 0.1,
|
fineStep: 0.1,
|
||||||
@@ -91,11 +122,17 @@ const props = withDefaults(defineProps<{
|
|||||||
lockKey: '',
|
lockKey: '',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
compact: false,
|
compact: false,
|
||||||
digits: 1
|
digits: 1,
|
||||||
|
editable: false,
|
||||||
|
unit: '',
|
||||||
|
dragRange: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const model = defineModel<number>({ required: true })
|
const model = defineModel<number>({ required: true })
|
||||||
const root = ref<HTMLElement | null>(null)
|
const root = ref<HTMLElement | null>(null)
|
||||||
|
const editInput = ref<HTMLInputElement | null>(null)
|
||||||
|
const editing = ref(false)
|
||||||
|
const draft = ref('')
|
||||||
const locked = ref(false)
|
const locked = ref(false)
|
||||||
|
|
||||||
const dialName = computed(() => props.label || props.ariaLabel || 'dial')
|
const dialName = computed(() => props.label || props.ariaLabel || 'dial')
|
||||||
@@ -170,6 +207,37 @@ function onKey(event: KeyboardEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coarsePointer(event: PointerEvent) {
|
||||||
|
if (event.pointerType === 'touch') return true
|
||||||
|
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return false
|
||||||
|
return window.matchMedia('(pointer: coarse)').matches
|
||||||
|
}
|
||||||
|
|
||||||
|
function dragGain(event: PointerEvent) {
|
||||||
|
return coarsePointer(event) ? 0.25 : 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function startEdit() {
|
||||||
|
if (!props.editable || props.disabled) return
|
||||||
|
draft.value = format(model.value)
|
||||||
|
editing.value = true
|
||||||
|
nextTick(() => {
|
||||||
|
editInput.value?.focus()
|
||||||
|
editInput.value?.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitEdit() {
|
||||||
|
if (!editing.value) return
|
||||||
|
const parsed = Number.parseFloat(draft.value.replace(',', '.').trim())
|
||||||
|
if (Number.isFinite(parsed)) model.value = clamp(parsed)
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelEdit() {
|
||||||
|
editing.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function onPointerDown(event: PointerEvent) {
|
function onPointerDown(event: PointerEvent) {
|
||||||
if (frozen.value || event.button !== 0) return
|
if (frozen.value || event.button !== 0) return
|
||||||
const el = root.value
|
const el = root.value
|
||||||
@@ -179,9 +247,11 @@ function onPointerDown(event: PointerEvent) {
|
|||||||
const height = el.clientHeight || 1
|
const height = el.clientHeight || 1
|
||||||
const startY = event.clientY
|
const startY = event.clientY
|
||||||
const startValue = model.value
|
const startValue = model.value
|
||||||
|
const gain = dragGain(event)
|
||||||
|
const travel = props.dragRange > 0 ? props.dragRange : span.value
|
||||||
const onMove = (move: PointerEvent) => {
|
const onMove = (move: PointerEvent) => {
|
||||||
const delta = startY - move.clientY
|
const delta = startY - move.clientY
|
||||||
model.value = clamp(startValue + (delta / height) * span.value)
|
model.value = clamp(startValue + (delta / height) * travel * gain)
|
||||||
}
|
}
|
||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
el.removeEventListener('pointermove', onMove)
|
el.removeEventListener('pointermove', onMove)
|
||||||
|
|||||||
+3
-3
@@ -1034,8 +1034,8 @@
|
|||||||
v-model="duration"
|
v-model="duration"
|
||||||
label="Duration"
|
label="Duration"
|
||||||
:hint="shotScriptMode
|
:hint="shotScriptMode
|
||||||
? 'This length applies to the initial shot and every extension in the script.'
|
? 'This length applies to the initial shot and every extension in the script. Tap the digits to type.'
|
||||||
: 'Type any length, or hover the wheel and scroll. Shift-scroll jumps by 1s. Drag up and down.'"
|
: 'Tap the digits to type. Hover and scroll ±0.1s, Shift-scroll ±1s. Drag the wheel.'"
|
||||||
/>
|
/>
|
||||||
<div class="text-sm">
|
<div class="text-sm">
|
||||||
<Thumbwheel
|
<Thumbwheel
|
||||||
@@ -1427,7 +1427,7 @@
|
|||||||
<DurationField
|
<DurationField
|
||||||
v-model="extendDuration"
|
v-model="extendDuration"
|
||||||
label="Extension duration"
|
label="Extension duration"
|
||||||
hint="Type any length, or hover the wheel and scroll. Shift-scroll jumps by 1s."
|
hint="Tap the digits to type. Hover and scroll ±0.1s, Shift-scroll ±1s."
|
||||||
/>
|
/>
|
||||||
<LoraSelect
|
<LoraSelect
|
||||||
v-model="extendLoraStack"
|
v-model="extendLoraStack"
|
||||||
|
|||||||
Reference in New Issue
Block a user