0.11.1: drawer mouse trigger and clip track / audio behavior
This commit is contained in:
1 parent
8d987d1526
commit
fa66ba69bb
9 files changed
+258
-31
No files matched your search
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "ruwio",
|
"name": "ruwio",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.11.0",
|
"version": "0.11.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -1016,7 +1016,7 @@ h4 {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
color: var(--text-dim);
|
color: var(--text-dim);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,24 @@ import { PhArrowClockwise, PhCaretLeft, PhCaretRight, PhFolderOpen, PhImagesSqua
|
|||||||
import { useClipartStore } from '@/stores/clipart'
|
import { useClipartStore } from '@/stores/clipart'
|
||||||
import { readAudioDuration, readAsDataUrl } from '@/lib/fileLoader'
|
import { readAudioDuration, readAsDataUrl } from '@/lib/fileLoader'
|
||||||
import { clearAudioDragSource, setAudioDragSource } from '@/composables/useAudioDragSource'
|
import { clearAudioDragSource, setAudioDragSource } from '@/composables/useAudioDragSource'
|
||||||
|
import { useFullscreen } from '@/composables/useFullscreen'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
const clipart = useClipartStore()
|
const clipart = useClipartStore()
|
||||||
|
const { isFullscreen } = useFullscreen()
|
||||||
|
|
||||||
|
// In browser-fullscreen mode the drawer auto-opens on tab hover and
|
||||||
|
// auto-closes when the mouse leaves the drawer — no click-toggle.
|
||||||
|
function onTabClick() {
|
||||||
|
if (isFullscreen.value) return
|
||||||
|
clipart.toggle()
|
||||||
|
}
|
||||||
|
function onTabEnter() {
|
||||||
|
if (isFullscreen.value) clipart.setOpen(true)
|
||||||
|
}
|
||||||
|
function onDrawerLeave() {
|
||||||
|
if (isFullscreen.value) clipart.setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
clipart.initialize()
|
clipart.initialize()
|
||||||
@@ -64,11 +79,12 @@ function onItemDragStart(e: DragEvent, index: number) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside class="clipart-drawer" :class="{ open: clipart.open }">
|
<aside class="clipart-drawer" :class="{ open: clipart.open }" @mouseleave="onDrawerLeave">
|
||||||
<button
|
<button
|
||||||
class="drawer-tab"
|
class="drawer-tab"
|
||||||
v-tooltip="clipart.open ? t('clipart.close') : t('clipart.open')"
|
v-tooltip="clipart.open ? t('clipart.close') : t('clipart.open')"
|
||||||
@click="clipart.toggle()"
|
@click="onTabClick"
|
||||||
|
@mouseenter="onTabEnter"
|
||||||
>
|
>
|
||||||
<component :is="clipart.open ? PhCaretLeft : PhCaretRight" :size="14" />
|
<component :is="clipart.open ? PhCaretLeft : PhCaretRight" :size="14" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -4,9 +4,22 @@ import { PhCaretLeft, PhCaretRight, PhCopy, PhFilmSlate, PhPlus, PhTrash } from
|
|||||||
import { useSceneStore } from '@/stores/scene'
|
import { useSceneStore } from '@/stores/scene'
|
||||||
import { confirmDialog, promptDialog } from '@/components/ui'
|
import { confirmDialog, promptDialog } from '@/components/ui'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
import { useFullscreen } from '@/composables/useFullscreen'
|
||||||
|
|
||||||
const store = useSceneStore()
|
const store = useSceneStore()
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
const { isFullscreen } = useFullscreen()
|
||||||
|
|
||||||
|
// In browser-fullscreen mode the drawer auto-opens on tab hover and
|
||||||
|
// auto-closes when the mouse leaves the drawer — there's no chrome to
|
||||||
|
// host a toggle button comfortably. In regular view the click-toggle
|
||||||
|
// behavior stays.
|
||||||
|
function onTabEnter() {
|
||||||
|
if (isFullscreen.value) open.value = true
|
||||||
|
}
|
||||||
|
function onDrawerLeave() {
|
||||||
|
if (isFullscreen.value) open.value = false
|
||||||
|
}
|
||||||
|
|
||||||
interface MenuState {
|
interface MenuState {
|
||||||
pageId: string
|
pageId: string
|
||||||
@@ -17,6 +30,7 @@ const menu = ref<MenuState | null>(null)
|
|||||||
const menuRef = ref<HTMLDivElement | null>(null)
|
const menuRef = ref<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
|
if (isFullscreen.value) return
|
||||||
open.value = !open.value
|
open.value = !open.value
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,11 +100,12 @@ watch(menu, (m) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside class="scene-drawer" :class="{ open }">
|
<aside class="scene-drawer" :class="{ open }" @mouseleave="onDrawerLeave">
|
||||||
<button
|
<button
|
||||||
class="drawer-tab"
|
class="drawer-tab"
|
||||||
v-tooltip="open ? t('scenes.close') : t('scenes.open')"
|
v-tooltip="open ? t('scenes.close') : t('scenes.open')"
|
||||||
@click="toggle"
|
@click="toggle"
|
||||||
|
@mouseenter="onTabEnter"
|
||||||
>
|
>
|
||||||
<component :is="open ? PhCaretRight : PhCaretLeft" :size="14" />
|
<component :is="open ? PhCaretRight : PhCaretLeft" :size="14" />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
+160
-21
@@ -1835,6 +1835,24 @@ function clipPeaks(clip: AudioClip): Float32Array | undefined {
|
|||||||
* the clip's current width (resizes on zoom + trim changes). Cheap
|
* the clip's current width (resizes on zoom + trim changes). Cheap
|
||||||
* because peaks are cached and the draw itself is a few hundred
|
* because peaks are cached and the draw itself is a few hundred
|
||||||
* fillRects. */
|
* fillRects. */
|
||||||
|
function waveformColorFor(clip: AudioClip): string | undefined {
|
||||||
|
// Honor the track color so tinted audio tracks get a matching
|
||||||
|
// waveform fill. Accept #rgb/#rrggbb and append a fixed 55% alpha
|
||||||
|
// (matches the default sky-blue tint). Non-hex values fall back to
|
||||||
|
// the drawWaveform default.
|
||||||
|
const track = audioTrackById.value.get(clip.audioTrackId)
|
||||||
|
const color = track?.color
|
||||||
|
if (!color) return undefined
|
||||||
|
const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(color)
|
||||||
|
if (short) {
|
||||||
|
const [, r, g, b] = short
|
||||||
|
return `#${r}${r}${g}${g}${b}${b}8c`
|
||||||
|
}
|
||||||
|
const full = /^#([0-9a-f]{6})$/i.exec(color)
|
||||||
|
if (full) return `#${full[1]}8c`
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
function renderWaveformCanvas(clip: AudioClip) {
|
function renderWaveformCanvas(clip: AudioClip) {
|
||||||
return (raw: Element | ComponentPublicInstance | null) => {
|
return (raw: Element | ComponentPublicInstance | null) => {
|
||||||
if (!raw || !(raw instanceof HTMLCanvasElement)) return
|
if (!raw || !(raw instanceof HTMLCanvasElement)) return
|
||||||
@@ -1852,7 +1870,7 @@ function renderWaveformCanvas(clip: AudioClip) {
|
|||||||
if (el.height !== pxH) el.height = pxH
|
if (el.height !== pxH) el.height = pxH
|
||||||
el.style.width = cssW + 'px'
|
el.style.width = cssW + 'px'
|
||||||
el.style.height = cssH + 'px'
|
el.style.height = cssH + 'px'
|
||||||
drawWaveform(el, peaks, { pixelRatio: pr })
|
drawWaveform(el, peaks, { pixelRatio: pr, color: waveformColorFor(clip) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2099,7 +2117,78 @@ function onAudioClipPointerUp(e: PointerEvent) {
|
|||||||
}
|
}
|
||||||
audioClipMove.value = null
|
audioClipMove.value = null
|
||||||
audioClipGhost.value = null
|
audioClipGhost.value = null
|
||||||
if (wasMoved) e.preventDefault()
|
}
|
||||||
|
|
||||||
|
interface AudioClipTrimState {
|
||||||
|
clipId: string
|
||||||
|
side: 'start' | 'end'
|
||||||
|
pointerId: number
|
||||||
|
startX: number
|
||||||
|
initialStartTime: number
|
||||||
|
initialTrimStart: number
|
||||||
|
initialTrimEnd: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const audioClipTrim = ref<AudioClipTrimState | null>(null)
|
||||||
|
|
||||||
|
function onAudioClipHandlePointerDown(
|
||||||
|
e: PointerEvent,
|
||||||
|
clip: AudioClip,
|
||||||
|
side: 'start' | 'end',
|
||||||
|
) {
|
||||||
|
if (e.button !== 0) return
|
||||||
|
const track = audioTrackById.value.get(clip.audioTrackId)
|
||||||
|
if (track?.locked) return
|
||||||
|
e.preventDefault()
|
||||||
|
e.stopPropagation()
|
||||||
|
const el = e.currentTarget as HTMLElement
|
||||||
|
el.setPointerCapture(e.pointerId)
|
||||||
|
audioClipTrim.value = {
|
||||||
|
clipId: clip.id,
|
||||||
|
side,
|
||||||
|
pointerId: e.pointerId,
|
||||||
|
startX: e.clientX,
|
||||||
|
initialStartTime: clip.startTime,
|
||||||
|
initialTrimStart: clip.trimStart ?? 0,
|
||||||
|
initialTrimEnd: clip.trimEnd ?? 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAudioClipHandlePointerMove(e: PointerEvent) {
|
||||||
|
const tr = audioClipTrim.value
|
||||||
|
if (!tr || tr.pointerId !== e.pointerId) return
|
||||||
|
const pps = effectivePxPerSec.value
|
||||||
|
if (pps <= 0) return
|
||||||
|
const rawDelta = (e.clientX - tr.startX) / pps
|
||||||
|
const clip = store.scene.audioClips?.find((c) => c.id === tr.clipId)
|
||||||
|
if (!clip) return
|
||||||
|
const asset = audioAssetById.value.get(clip.audioAssetId)
|
||||||
|
if (!asset) return
|
||||||
|
const MIN_VISIBLE = 0.05
|
||||||
|
const maxBudget = asset.duration - MIN_VISIBLE
|
||||||
|
if (tr.side === 'start') {
|
||||||
|
// Clamp locally so overshooting the natural start doesn't slide
|
||||||
|
// the whole clip — matches the regular-clip handle behavior.
|
||||||
|
const minDelta = Math.max(-tr.initialTrimStart, -tr.initialStartTime)
|
||||||
|
const maxDelta = maxBudget - tr.initialTrimStart - tr.initialTrimEnd
|
||||||
|
const delta = Math.min(Math.max(rawDelta, minDelta), maxDelta)
|
||||||
|
store.trimAudioClip(tr.clipId, {
|
||||||
|
trimStart: tr.initialTrimStart + delta,
|
||||||
|
startTime: tr.initialStartTime + delta,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
store.trimAudioClip(tr.clipId, {
|
||||||
|
trimEnd: tr.initialTrimEnd - rawDelta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onAudioClipHandlePointerUp(e: PointerEvent) {
|
||||||
|
const tr = audioClipTrim.value
|
||||||
|
if (!tr || tr.pointerId !== e.pointerId) return
|
||||||
|
const el = e.currentTarget as HTMLElement
|
||||||
|
if (el.hasPointerCapture(e.pointerId)) el.releasePointerCapture(e.pointerId)
|
||||||
|
audioClipTrim.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Button asset edit bar --------------------------------------
|
// -------- Button asset edit bar --------------------------------------
|
||||||
@@ -2253,17 +2342,29 @@ function onClipHandlePointerMove(e: PointerEvent) {
|
|||||||
if (!t || t.pointerId !== e.pointerId) return
|
if (!t || t.pointerId !== e.pointerId) return
|
||||||
const pps = effectivePxPerSec.value
|
const pps = effectivePxPerSec.value
|
||||||
if (pps <= 0) return
|
if (pps <= 0) return
|
||||||
const deltaSec = (e.clientX - t.startX) / pps
|
const rawDelta = (e.clientX - t.startX) / pps
|
||||||
if (t.side === 'start') {
|
if (t.side === 'start') {
|
||||||
// shift block right = trim more from start + shift startTime
|
// Clamp the delta locally so pulling the handle past the natural
|
||||||
|
// start doesn't shift startTime — previously trimStart clamped to
|
||||||
|
// 0 in the store but startTime still moved, which slid the whole
|
||||||
|
// clip. Bounds: trimStart stays in [0, maxBudget - trimEnd] and
|
||||||
|
// we also can't shift startTime below 0.
|
||||||
|
const inst = store.scene.clipInstances?.find((ci) => ci.id === t.instanceId)
|
||||||
|
const clip = inst ? store.scene.clips?.find((c) => c.id === inst.clipId) : null
|
||||||
|
if (!clip) return
|
||||||
|
const MIN_VISIBLE = 0.05
|
||||||
|
const maxBudget = clip.duration - MIN_VISIBLE
|
||||||
|
const minDelta = Math.max(-t.initialTrimStart, -t.initialStartTime)
|
||||||
|
const maxDelta = maxBudget - t.initialTrimStart - t.initialTrimEnd
|
||||||
|
const delta = Math.min(Math.max(rawDelta, minDelta), maxDelta)
|
||||||
store.trimClipInstance(t.instanceId, {
|
store.trimClipInstance(t.instanceId, {
|
||||||
trimStart: t.initialTrimStart + deltaSec,
|
trimStart: t.initialTrimStart + delta,
|
||||||
startTime: t.initialStartTime + deltaSec,
|
startTime: t.initialStartTime + delta,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// shift right edge right = less trimEnd (reveal more)
|
// shift right edge right = less trimEnd (reveal more)
|
||||||
store.trimClipInstance(t.instanceId, {
|
store.trimClipInstance(t.instanceId, {
|
||||||
trimEnd: t.initialTrimEnd - deltaSec,
|
trimEnd: t.initialTrimEnd - rawDelta,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3310,7 +3411,12 @@ onBeforeUnmount(() => {
|
|||||||
v-for="clip in audioClipsByTrack[track.audioTrackId] ?? []"
|
v-for="clip in audioClipsByTrack[track.audioTrackId] ?? []"
|
||||||
:key="clip.id"
|
:key="clip.id"
|
||||||
class="audio-clip-block"
|
class="audio-clip-block"
|
||||||
:class="{ moving: audioClipMove?.clipId === clip.id }"
|
:class="{
|
||||||
|
moving: audioClipMove?.clipId === clip.id,
|
||||||
|
'trimmed-start': (clip.trimStart ?? 0) > 0,
|
||||||
|
'trimmed-end': (clip.trimEnd ?? 0) > 0,
|
||||||
|
trimming: audioClipTrim?.clipId === clip.id,
|
||||||
|
}"
|
||||||
:style="audioClipBlockStyle(clip)"
|
:style="audioClipBlockStyle(clip)"
|
||||||
v-tooltip="audioClipTooltip(clip)"
|
v-tooltip="audioClipTooltip(clip)"
|
||||||
@pointerdown="onAudioClipPointerDown($event, clip)"
|
@pointerdown="onAudioClipPointerDown($event, clip)"
|
||||||
@@ -3319,6 +3425,17 @@ onBeforeUnmount(() => {
|
|||||||
@pointercancel="onAudioClipPointerUp"
|
@pointercancel="onAudioClipPointerUp"
|
||||||
@contextmenu="onAudioClipContextMenu($event, clip)"
|
@contextmenu="onAudioClipContextMenu($event, clip)"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
class="clip-handle start"
|
||||||
|
draggable="false"
|
||||||
|
@pointerdown="onAudioClipHandlePointerDown($event, clip, 'start')"
|
||||||
|
@pointermove="onAudioClipHandlePointerMove"
|
||||||
|
@pointerup="onAudioClipHandlePointerUp"
|
||||||
|
@pointercancel="onAudioClipHandlePointerUp"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@dblclick.stop
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
<canvas
|
<canvas
|
||||||
v-if="clipPeaks(clip)"
|
v-if="clipPeaks(clip)"
|
||||||
class="audio-clip-wave"
|
class="audio-clip-wave"
|
||||||
@@ -3327,6 +3444,17 @@ onBeforeUnmount(() => {
|
|||||||
<span class="audio-clip-label">
|
<span class="audio-clip-label">
|
||||||
{{ audioAssetById.get(clip.audioAssetId)?.name ?? '—' }}
|
{{ audioAssetById.get(clip.audioAssetId)?.name ?? '—' }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
class="clip-handle end"
|
||||||
|
draggable="false"
|
||||||
|
@pointerdown="onAudioClipHandlePointerDown($event, clip, 'end')"
|
||||||
|
@pointermove="onAudioClipHandlePointerMove"
|
||||||
|
@pointerup="onAudioClipHandlePointerUp"
|
||||||
|
@pointercancel="onAudioClipHandlePointerUp"
|
||||||
|
@dragstart.prevent.stop
|
||||||
|
@dblclick.stop
|
||||||
|
@click.stop
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="audioClipGhost && audioClipGhost.targetTrackId === track.audioTrackId"
|
v-if="audioClipGhost && audioClipGhost.targetTrackId === track.audioTrackId"
|
||||||
@@ -4811,15 +4939,15 @@ onBeforeUnmount(() => {
|
|||||||
width: 9px;
|
width: 9px;
|
||||||
height: 9px;
|
height: 9px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #f59e0b;
|
background: var(--track-color, #f59e0b);
|
||||||
box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 0 0 1.5px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-frame-count {
|
.script-frame-count {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #f59e0b;
|
color: var(--track-color, #f59e0b);
|
||||||
background: rgba(245, 158, 11, 0.15);
|
background: color-mix(in srgb, var(--track-color, #f59e0b) 18%, transparent);
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
line-height: 13px;
|
line-height: 13px;
|
||||||
@@ -4833,8 +4961,8 @@ onBeforeUnmount(() => {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(14, 165, 233, 0.08) 0%,
|
color-mix(in srgb, var(--track-color, #0ea5e9) 8%, transparent) 0%,
|
||||||
rgba(14, 165, 233, 0.02) 100%
|
color-mix(in srgb, var(--track-color, #0ea5e9) 2%, transparent) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4876,10 +5004,10 @@ onBeforeUnmount(() => {
|
|||||||
bottom: 4px;
|
bottom: 4px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
to bottom,
|
to bottom,
|
||||||
rgba(14, 165, 233, 0.35) 0%,
|
color-mix(in srgb, var(--track-color, #0ea5e9) 35%, transparent) 0%,
|
||||||
rgba(14, 165, 233, 0.15) 100%
|
color-mix(in srgb, var(--track-color, #0ea5e9) 15%, transparent) 100%
|
||||||
);
|
);
|
||||||
border: 1px solid rgba(14, 165, 233, 0.6);
|
border: 1px solid color-mix(in srgb, var(--track-color, #0ea5e9) 60%, transparent);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -5034,29 +5162,40 @@ onBeforeUnmount(() => {
|
|||||||
right: 2px;
|
right: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-block:hover .clip-handle {
|
.clip-block:hover .clip-handle,
|
||||||
|
.audio-clip-block:hover .clip-handle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-block.trimmed-start .clip-handle.start,
|
.clip-block.trimmed-start .clip-handle.start,
|
||||||
.clip-block.trimmed-end .clip-handle.end {
|
.clip-block.trimmed-end .clip-handle.end,
|
||||||
|
.audio-clip-block.trimmed-start .clip-handle.start,
|
||||||
|
.audio-clip-block.trimmed-end .clip-handle.end {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-block.trimmed-start .clip-handle.start::after,
|
.clip-block.trimmed-start .clip-handle.start::after,
|
||||||
.clip-block.trimmed-end .clip-handle.end::after {
|
.clip-block.trimmed-end .clip-handle.end::after,
|
||||||
|
.audio-clip-block.trimmed-start .clip-handle.start::after,
|
||||||
|
.audio-clip-block.trimmed-end .clip-handle.end::after {
|
||||||
background: #ffc857;
|
background: #ffc857;
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-block.trimming .clip-handle {
|
.clip-block.trimming .clip-handle,
|
||||||
|
.audio-clip-block.trimming .clip-handle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clip-block.trimming {
|
.clip-block.trimming,
|
||||||
|
.audio-clip-block.trimming {
|
||||||
box-shadow: 0 0 0 1px #ffc857;
|
box-shadow: 0 0 0 1px #ffc857;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track.audio-track.locked .clip-handle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.clip-block.moving {
|
.clip-block.moving {
|
||||||
box-shadow: 0 0 0 1px var(--track-color, var(--accent)), 0 4px 12px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 0 0 1px var(--track-color, var(--accent)), 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
// Tracks whether the window is in browser fullscreen — covers both the
|
||||||
|
// Fullscreen API (element.requestFullscreen) and the F11-style display
|
||||||
|
// mode that Firefox uses without setting fullscreenElement. The drawers
|
||||||
|
// switch to hover-open behavior when this is true.
|
||||||
|
const isFullscreen = ref(false)
|
||||||
|
|
||||||
|
let listeners = 0
|
||||||
|
let mql: MediaQueryList | null = null
|
||||||
|
|
||||||
|
function evaluate() {
|
||||||
|
const apiFs = !!document.fullscreenElement
|
||||||
|
const mqFs = mql?.matches === true
|
||||||
|
isFullscreen.value = apiFs || mqFs
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFullscreenChange() {
|
||||||
|
evaluate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMqChange() {
|
||||||
|
evaluate()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFullscreen() {
|
||||||
|
onMounted(() => {
|
||||||
|
if (listeners === 0) {
|
||||||
|
mql = window.matchMedia('(display-mode: fullscreen)')
|
||||||
|
mql.addEventListener('change', onMqChange)
|
||||||
|
document.addEventListener('fullscreenchange', onFullscreenChange)
|
||||||
|
evaluate()
|
||||||
|
}
|
||||||
|
listeners += 1
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
listeners -= 1
|
||||||
|
if (listeners === 0) {
|
||||||
|
document.removeEventListener('fullscreenchange', onFullscreenChange)
|
||||||
|
mql?.removeEventListener('change', onMqChange)
|
||||||
|
mql = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { isFullscreen }
|
||||||
|
}
|
||||||
@@ -69,7 +69,7 @@ export const en = {
|
|||||||
thumbsToggle: 'Thumbs',
|
thumbsToggle: 'Thumbs',
|
||||||
imageLabel: 'Image',
|
imageLabel: 'Image',
|
||||||
addLayerTooltip: 'Insert as layer (or drag to stage)',
|
addLayerTooltip: 'Insert as layer (or drag to stage)',
|
||||||
noImages: 'No images imported',
|
noImages: 'No images imported yet',
|
||||||
clips: 'Clips',
|
clips: 'Clips',
|
||||||
createClipTooltip: 'Create new clip',
|
createClipTooltip: 'Create new clip',
|
||||||
clipLayerCount: '{n} layers',
|
clipLayerCount: '{n} layers',
|
||||||
@@ -88,7 +88,7 @@ export const en = {
|
|||||||
buttons: 'Buttons',
|
buttons: 'Buttons',
|
||||||
noButtons: 'No buttons yet',
|
noButtons: 'No buttons yet',
|
||||||
audio: 'Audio',
|
audio: 'Audio',
|
||||||
noAudio: 'No audio imported',
|
noAudio: 'No audio imported yet',
|
||||||
deleteAudioDialog: {
|
deleteAudioDialog: {
|
||||||
title: 'Delete audio?',
|
title: 'Delete audio?',
|
||||||
message: 'Remove "{name}" from the asset library. Any clips on the timeline that reference it will also be removed.',
|
message: 'Remove "{name}" from the asset library. Any clips on the timeline that reference it will also be removed.',
|
||||||
@@ -996,7 +996,7 @@ export const de: DeepPartial<Messages> = {
|
|||||||
thumbsToggle: 'Thumbs',
|
thumbsToggle: 'Thumbs',
|
||||||
imageLabel: 'Bild',
|
imageLabel: 'Bild',
|
||||||
addLayerTooltip: 'Als Ebene einfügen oder auf die Bühne ziehen',
|
addLayerTooltip: 'Als Ebene einfügen oder auf die Bühne ziehen',
|
||||||
noImages: 'Keine Bilder importiert',
|
noImages: 'Noch keine Bilder importiert',
|
||||||
clips: 'Clips',
|
clips: 'Clips',
|
||||||
createClipTooltip: 'Neuen Clip anlegen',
|
createClipTooltip: 'Neuen Clip anlegen',
|
||||||
clipLayerCount: '{n} Ebenen',
|
clipLayerCount: '{n} Ebenen',
|
||||||
@@ -1015,7 +1015,7 @@ export const de: DeepPartial<Messages> = {
|
|||||||
buttons: 'Buttons',
|
buttons: 'Buttons',
|
||||||
noButtons: 'Noch keine Buttons',
|
noButtons: 'Noch keine Buttons',
|
||||||
audio: 'Audio',
|
audio: 'Audio',
|
||||||
noAudio: 'Kein Audio importiert',
|
noAudio: 'Noch kein Audio importiert',
|
||||||
deleteAudioDialog: {
|
deleteAudioDialog: {
|
||||||
title: 'Audio löschen?',
|
title: 'Audio löschen?',
|
||||||
message:
|
message:
|
||||||
|
|||||||
@@ -1010,6 +1010,18 @@ export const useSceneStore = defineStore('scene', {
|
|||||||
scriptTrack.color = color
|
scriptTrack.color = color
|
||||||
scriptTrack.colorOverridden = true
|
scriptTrack.colorOverridden = true
|
||||||
}
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const audioTrack = this.scene.audioTracks?.find((t) => t.id === trackId)
|
||||||
|
if (audioTrack) {
|
||||||
|
// Audio tracks are never inside a group either.
|
||||||
|
if (color === null) {
|
||||||
|
delete audioTrack.color
|
||||||
|
delete audioTrack.colorOverridden
|
||||||
|
} else {
|
||||||
|
audioTrack.color = color
|
||||||
|
audioTrack.colorOverridden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shiftKeyframesPosition(
|
shiftKeyframesPosition(
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
export const APP_VERSION = '0.11.0'
|
export const APP_VERSION = '0.11.1'
|
||||||
export const APP_NAME = 'RUWIO'
|
export const APP_NAME = 'RUWIO'
|
||||||
Reference in new issue
Block a user