From 0f6e840807fe4db27f2c0634193ffe155b8a7080 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kr=C3=BChlmann?= Date: Thu, 11 Dec 2025 13:34:49 +0100 Subject: [PATCH] Add keyboard controls --- src/main/resources/application.yml | 2 + src/main/resources/static/css/styles.css | 3 +- src/main/resources/static/js/admin.js | 58 ++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 41e7bc1..d5d59b8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -27,6 +27,8 @@ spring: driver-class-name: org.sqlite.JDBC hikari: connection-init-sql: "PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;" + maximum-pool-size: 1 + minimum-idle: 1 jpa: hibernate: ddl-auto: update diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index d83638e..5ac9e08 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -1247,7 +1247,6 @@ body { .asset-item.pending { cursor: default; - border-style: dashed; opacity: 0.92; } @@ -1682,7 +1681,7 @@ body { .toast-container { position: fixed; - top: 16px; + bottom: 16px; right: 16px; display: flex; flex-direction: column; diff --git a/src/main/resources/static/js/admin.js b/src/main/resources/static/js/admin.js index a6ee305..19e0b47 100644 --- a/src/main/resources/static/js/admin.js +++ b/src/main/resources/static/js/admin.js @@ -26,6 +26,8 @@ const MAX_VOLUME = 2; const VOLUME_SLIDER_MAX = 200; const VOLUME_CURVE_STRENGTH = -0.6; const pendingTransformSaves = new Map(); +const KEYBOARD_NUDGE_STEP = 5; +const KEYBOARD_NUDGE_FAST_STEP = 20; const controlsPanel = document.getElementById('asset-controls'); @@ -84,6 +86,13 @@ function debounce(fn, wait = 150) { }; } +function isFormInputElement(element) { + if (!element) return false; + if (element.isContentEditable) return true; + const tag = element.tagName ? element.tagName.toLowerCase() : ''; + return ['input', 'textarea', 'select', 'button', 'option'].includes(tag); +} + function schedulePersistTransform(asset, silent = false, delay = 200) { if (!asset?.id) return; cancelPendingTransform(asset.id); @@ -349,6 +358,55 @@ if (selectedDeleteBtn) { deleteAsset(asset); }); } + +window.addEventListener('keydown', (event) => { + if (isFormInputElement(event.target)) { + return; + } + + const asset = getSelectedAsset(); + + if ((event.key === 'Delete' || event.key === 'Backspace') && asset) { + event.preventDefault(); + deleteAsset(asset); + return; + } + + if (!asset || isAudioAsset(asset)) { + return; + } + + const step = event.shiftKey ? KEYBOARD_NUDGE_FAST_STEP : KEYBOARD_NUDGE_STEP; + let moved = false; + + switch (event.key) { + case 'ArrowUp': + asset.y -= step; + moved = true; + break; + case 'ArrowDown': + asset.y += step; + moved = true; + break; + case 'ArrowLeft': + asset.x -= step; + moved = true; + break; + case 'ArrowRight': + asset.x += step; + moved = true; + break; + default: + break; + } + + if (moved) { + event.preventDefault(); + updateRenderState(asset); + schedulePersistTransform(asset); + drawAndList(); + } +}); function connect() { const socket = new SockJS('/ws'); stompClient = Stomp.over(socket);