From 7d81a50e74b8d1aaa878919d1a49f8277de666fa Mon Sep 17 00:00:00 2001 From: th Date: Sun, 26 Apr 2026 20:01:54 +0200 Subject: [PATCH] 1.2.9: global seconds/frames toggle + sane number-input widths/rounding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Timeline ruler's existing s/fr toggle (store.editor.timelineDisplayMode) now drives every time display and time input across the editor. Mixing "frames here, seconds there" was inconsistent — pick the unit once, see it everywhere. New composable (src/composables/useTimeFormat.ts): - formatTime(seconds, opts) → "1.42s" or "42 fr" (short variant drops trailing zeros for ruler-style ticks). - timeToInputValue / inputValueToSeconds — paired conversions used at every bound to a time. Storage stays in seconds (renderer + .ruw files unchanged); conversion happens at the UI edge. - inputStep ('0.01' / '1') and unitLabel ('seconds' / 'frames') for consistent input attributes / labels. Surfaces switched over: - App.vue topbar playhead + duration. - PropertiesPanel keyframe-here label (`@ 1.50s` → `@ 45 fr`). - FullscreenPreview time label. - AssetSidebar clip + audio asset durations. - Timeline audio clip + clip trim tooltips (route through formatKfTime, which already honoured the toggle). - DocumentDrawer scene duration input — step + min flip with the mode, label gets a (seconds)/(frames) suffix. - ScriptFrameDialog time input — same flip. - ActionEditor jumpTo time input — same flip. (jumpToFrame stays as frames since "frame" is its semantic, not a unit choice.) - Kaleido offset on ClipInstance — now follows the global toggle too, was hardcoded frames before. The frames-by-default UX comes from the user defaulting to 'frames' on the ruler; switching to seconds shows the same offset in seconds without losing precision. Number-input audit (cleaning up "ewig viele Nachkommastellen"): - ScriptFrameDialog time: rounded display to ≤3 decimals at the edge. - ActionEditor jumpTo time: same. - PropertiesPanel kaleido pivot X/Y (ClipInstance): rounded to int. - RangeSlider strokeWidth format: snapped to 0.5 increments. Number-input widths (capping the panel sprawl): - PropertiesPanel inputs: max-width 96px + tabular-nums; .row inputs (side-by-side pivot X/Y, transform X/Y) keep flex-share. - ActionEditor .params input[type='number']: explicit 96px. - ScriptFrameDialog .row input[type='number']: explicit 96px. - DocumentDrawer + ExportDialog: already had explicit widths. Tests (6 new under "time format — global seconds/frames toggle"): - useTimeFormat composable roundtrips through both modes at fps=30. - PropertiesPanel keyframe label flips on mode change. - DocumentDrawer duration input flips step + value (2s ↔ 60fr). - ActionEditor jumpTo flips step + value (1s ↔ 24fr at fps=24). - Kaleido offset display switches between modes for the same stored value (0.5s ↔ 15fr). - Kaleido offset write-back: typing "30" in frames mode at fps=30 stores exactly 1.0s. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- src/App.vue | 4 +- src/components/ActionEditor.vue | 16 ++- src/components/AssetSidebar.vue | 6 +- src/components/DocumentDrawer.vue | 20 ++- src/components/FullscreenPreview.vue | 5 +- src/components/PropertiesPanel.vue | 57 +++++--- src/components/ScriptFrameDialog.vue | 16 ++- src/components/Timeline.vue | 8 +- src/composables/useTimeFormat.ts | 79 +++++++++++ src/devtools/tests.ts | 202 +++++++++++++++++++++++++++ src/i18n/messages.ts | 2 +- src/version.ts | 2 +- 13 files changed, 373 insertions(+), 46 deletions(-) create mode 100644 src/composables/useTimeFormat.ts diff --git a/package.json b/package.json index 17870f9..fbc20b7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ruwio", "private": true, - "version": "1.2.8", + "version": "1.2.9", "type": "module", "scripts": { "dev": "vite", diff --git a/src/App.vue b/src/App.vue index 451598e..044bf82 100644 --- a/src/App.vue +++ b/src/App.vue @@ -44,6 +44,7 @@ import { useHistory } from './composables/useHistory' import { useAudioRuntime } from './composables/useAudioRuntime' import { useDocumentDrawer } from './composables/useDocumentDrawer' import { usePlaybackLoop } from './composables/usePlaybackLoop' +import { useTimeFormat } from './composables/useTimeFormat' import { useProjectName } from './composables/useProjectName' import { shouldChromeClickDeselect } from './lib/chromeClick' import { renderSceneToStillImage } from './lib/stillImageExport' @@ -165,6 +166,7 @@ const isDirty = ref(false) usePlaybackLoop() useAudioRuntime() const store = useSceneStore() +const { formatTime } = useTimeFormat() const clipart = useClipartStore() const history = useHistory() const deselectAll = useDeselectAll() @@ -631,7 +633,7 @@ onBeforeUnmount(() => { :title="t('app.stop')" @click="stop" /> - {{ store.playhead.toFixed(2) }}s / {{ store.scene.duration.toFixed(2) }}s + {{ formatTime(store.playhead) }} / {{ formatTime(store.scene.duration) }}
diff --git a/src/components/ActionEditor.vue b/src/components/ActionEditor.vue index 939eccd..20e32a7 100644 --- a/src/components/ActionEditor.vue +++ b/src/components/ActionEditor.vue @@ -9,6 +9,9 @@ import { import { useSceneStore } from '@/stores/scene' import { t } from '@/i18n' import type { Action } from '@/types/scene' +import { useTimeFormat } from '@/composables/useTimeFormat' + +const { timeToInputValue, inputValueToSeconds, inputStep, unitLabel } = useTimeFormat() const props = defineProps<{ actions: Action[] }>() const emit = defineEmits<{ 'update:actions': [value: Action[]] }>() @@ -154,13 +157,13 @@ function onTypeChange(index: number, newType: Action['type']) {
@@ -363,6 +366,11 @@ function onTypeChange(index: number, newType: Action['type']) { font-family: inherit; } +.params input[type='number'] { + width: 96px; + font-variant-numeric: tabular-nums; +} + .params textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; diff --git a/src/components/AssetSidebar.vue b/src/components/AssetSidebar.vue index 69760b9..216c5e4 100644 --- a/src/components/AssetSidebar.vue +++ b/src/components/AssetSidebar.vue @@ -21,6 +21,7 @@ import { } from '@phosphor-icons/vue' import { useSceneStore } from '@/stores/scene' import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref' +import { useTimeFormat } from '@/composables/useTimeFormat' import { importAudioFile, importImageFile, isAudioFile } from '@/lib/fileLoader' import { clearAudioDragSource, @@ -42,6 +43,7 @@ const fileInput = ref(null) const uploadError = ref(null) const { enabled: showThumbs } = useAssetThumbsPref() +const { formatTime } = useTimeFormat() /** Per-section collapsed state, persisted so the user's layout * survives reloads. Each key is false (expanded) by default — @@ -782,7 +784,7 @@ function onAssetDragStart(e: DragEvent, asset: ImageOrSvgAsset) {
{{ clip.name }} - {{ t('sidebar.clipLayerCount', { n: clip.layers.length }) }} · {{ clip.duration.toFixed(1) }}s + {{ t('sidebar.clipLayerCount', { n: clip.layers.length }) }} · {{ formatTime(clip.duration, { short: true, decimals: 1 }) }}
@@ -882,7 +884,7 @@ function onAssetDragStart(e: DragEvent, asset: ImageOrSvgAsset) {
{{ asset.name }} - {{ asset.duration.toFixed(2) }}s + {{ formatTime(asset.duration) }}