Separate asset select panel

This commit is contained in:
2025-12-10 09:46:54 +01:00
parent b178c68434
commit ab84747bbf
3 changed files with 233 additions and 102 deletions

View File

@@ -408,6 +408,35 @@ body {
padding: 18px; padding: 18px;
} }
.asset-management {
display: grid;
grid-template-columns: 1.25fr 1fr;
gap: 16px;
margin-top: 14px;
align-items: start;
}
.asset-column {
display: flex;
flex-direction: column;
gap: 12px;
}
.asset-column.inspector {
position: sticky;
top: 12px;
}
@media (max-width: 1024px) {
.asset-management {
grid-template-columns: 1fr;
}
.asset-column.inspector {
position: static;
}
}
.controls ul { .controls ul {
list-style: none; list-style: none;
padding: 0; padding: 0;
@@ -503,6 +532,15 @@ body {
margin: 0; margin: 0;
} }
.asset-inspector {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(148, 163, 184, 0.15);
}
.asset-controls-placeholder {
margin-top: 12px;
}
.selected-asset-banner { .selected-asset-banner {
display: grid; display: grid;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
@@ -680,6 +718,11 @@ body {
gap: 8px; gap: 8px;
} }
.asset-inspector .selected-asset-actions .icon-button,
.asset-inspector .selected-asset-actions .icon-button:disabled {
background: #0f172a;
}
.asset-meta-badges { .asset-meta-badges {
margin-top: 4px; margin-top: 4px;
} }

View File

@@ -36,6 +36,12 @@ const audioPitchInput = document.getElementById('asset-audio-pitch');
const audioVolumeInput = document.getElementById('asset-audio-volume'); const audioVolumeInput = document.getElementById('asset-audio-volume');
const controlsPlaceholder = document.getElementById('asset-controls-placeholder'); const controlsPlaceholder = document.getElementById('asset-controls-placeholder');
const fileNameLabel = document.getElementById('asset-file-name'); const fileNameLabel = document.getElementById('asset-file-name');
const assetInspector = document.getElementById('asset-inspector');
const selectedAssetName = document.getElementById('selected-asset-name');
const selectedAssetMeta = document.getElementById('selected-asset-meta');
const selectedAssetBadges = document.getElementById('selected-asset-badges');
const selectedVisibilityBtn = document.getElementById('selected-asset-visibility');
const selectedDeleteBtn = document.getElementById('selected-asset-delete');
const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80"><rect width="100%" height="100%" fill="#1f2937" rx="8"/><g fill="#fbbf24" transform="translate(20 20)"><circle cx="15" cy="20" r="6"/><rect x="28" y="5" width="12" height="30" rx="2"/><rect x="45" y="10" width="140" height="5" fill="#fef3c7"/><rect x="45" y="23" width="110" height="5" fill="#fef3c7"/></g><text x="20" y="70" fill="#e5e7eb" font-family="sans-serif" font-size="14">Audio</text></svg>'); const audioPlaceholder = 'data:image/svg+xml;utf8,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" width="320" height="80"><rect width="100%" height="100%" fill="#1f2937" rx="8"/><g fill="#fbbf24" transform="translate(20 20)"><circle cx="15" cy="20" r="6"/><rect x="28" y="5" width="12" height="30" rx="2"/><rect x="45" y="10" width="140" height="5" fill="#fef3c7"/><rect x="45" y="23" width="110" height="5" fill="#fef3c7"/></g><text x="20" y="70" fill="#e5e7eb" font-family="sans-serif" font-size="14">Audio</text></svg>');
const aspectLockState = new Map(); const aspectLockState = new Map();
const commitSizeChange = debounce(() => applyTransformFromInputs(), 180); const commitSizeChange = debounce(() => applyTransformFromInputs(), 180);
@@ -59,6 +65,20 @@ if (audioDelayInput) audioDelayInput.addEventListener('input', updateAudioSettin
if (audioSpeedInput) audioSpeedInput.addEventListener('input', updateAudioSettingsFromInputs); if (audioSpeedInput) audioSpeedInput.addEventListener('input', updateAudioSettingsFromInputs);
if (audioPitchInput) audioPitchInput.addEventListener('input', updateAudioSettingsFromInputs); if (audioPitchInput) audioPitchInput.addEventListener('input', updateAudioSettingsFromInputs);
if (audioVolumeInput) audioVolumeInput.addEventListener('input', updateAudioSettingsFromInputs); if (audioVolumeInput) audioVolumeInput.addEventListener('input', updateAudioSettingsFromInputs);
if (selectedVisibilityBtn) {
selectedVisibilityBtn.addEventListener('click', () => {
const asset = getSelectedAsset();
if (!asset) return;
updateVisibility(asset, !asset.hidden);
});
}
if (selectedDeleteBtn) {
selectedDeleteBtn.addEventListener('click', () => {
const asset = getSelectedAsset();
if (!asset) return;
deleteAsset(asset);
});
}
function connect() { function connect() {
const socket = new SockJS('/ws'); const socket = new SockJS('/ws');
stompClient = Stomp.over(socket); stompClient = Stomp.over(socket);
@@ -824,6 +844,9 @@ function renderAssetList() {
if (!assets.size) { if (!assets.size) {
selectedAssetId = null; selectedAssetId = null;
if (assetInspector) {
assetInspector.classList.add('hidden');
}
const empty = document.createElement('li'); const empty = document.createElement('li');
empty.textContent = 'No assets yet. Upload to get started.'; empty.textContent = 'No assets yet. Upload to get started.';
list.appendChild(empty); list.appendChild(empty);
@@ -831,6 +854,10 @@ function renderAssetList() {
return; return;
} }
if (assetInspector) {
assetInspector.classList.remove('hidden');
}
const sortedAssets = getChronologicalAssets(); const sortedAssets = getChronologicalAssets();
sortedAssets.forEach((asset) => { sortedAssets.forEach((asset) => {
const li = document.createElement('li'); const li = document.createElement('li');
@@ -903,18 +930,10 @@ function renderAssetList() {
}); });
li.appendChild(row); li.appendChild(row);
if (asset.id === selectedAssetId && controlsPanel) {
controlsPanel.classList.remove('hidden');
const detail = document.createElement('div');
detail.className = 'asset-detail';
detail.appendChild(controlsPanel);
li.appendChild(detail);
updateSelectedAssetControls(asset);
}
list.appendChild(li); list.appendChild(li);
}); });
updateSelectedAssetControls();
} }
function createBadge(label, extraClass = '') { function createBadge(label, extraClass = '') {
@@ -957,6 +976,12 @@ function getSelectedAsset() {
} }
function updateSelectedAssetControls(asset = getSelectedAsset()) { function updateSelectedAssetControls(asset = getSelectedAsset()) {
if (controlsPlaceholder && controlsPanel && controlsPanel.parentElement !== controlsPlaceholder) {
controlsPlaceholder.appendChild(controlsPanel);
}
updateSelectedAssetSummary(asset);
if (!controlsPanel || !asset) { if (!controlsPanel || !asset) {
if (controlsPanel) controlsPanel.classList.add('hidden'); if (controlsPanel) controlsPanel.classList.add('hidden');
return; return;
@@ -1001,6 +1026,43 @@ function updateSelectedAssetControls(asset = getSelectedAsset()) {
} }
} }
function updateSelectedAssetSummary(asset) {
if (assetInspector) {
assetInspector.classList.toggle('hidden', !asset && !assets.size);
}
if (selectedAssetName) {
selectedAssetName.textContent = asset ? (asset.name || `Asset ${asset.id.slice(0, 6)}`) : 'Choose an asset';
}
if (selectedAssetMeta) {
selectedAssetMeta.textContent = asset
? `${Math.round(asset.width)}x${Math.round(asset.height)} · Layer ${asset.zIndex ?? 1}`
: 'Pick an asset in the list to adjust its placement and playback.';
}
if (selectedAssetBadges) {
selectedAssetBadges.innerHTML = '';
if (asset) {
selectedAssetBadges.appendChild(createBadge(asset.hidden ? 'Hidden' : 'Visible', asset.hidden ? 'danger' : ''));
selectedAssetBadges.appendChild(createBadge(getDisplayMediaType(asset)));
const aspectLabel = formatAspectRatioLabel(asset);
if (aspectLabel) {
selectedAssetBadges.appendChild(createBadge(aspectLabel, 'subtle'));
}
}
}
if (selectedVisibilityBtn) {
selectedVisibilityBtn.disabled = !asset;
selectedVisibilityBtn.title = asset ? (asset.hidden ? 'Show asset' : 'Hide asset') : 'Toggle visibility';
selectedVisibilityBtn.innerHTML = asset
? `<i class="fa-solid ${asset.hidden ? 'fa-eye' : 'fa-eye-slash'}"></i>`
: '<i class="fa-solid fa-eye-slash"></i>';
}
if (selectedDeleteBtn) {
selectedDeleteBtn.disabled = !asset;
selectedDeleteBtn.title = asset ? 'Delete asset' : 'Delete asset';
}
}
function applyTransformFromInputs() { function applyTransformFromInputs() {
const asset = getSelectedAsset(); const asset = getSelectedAsset();
if (!asset) return; if (!asset) return;

View File

@@ -27,102 +27,128 @@
<div class="controls-full panel"> <div class="controls-full panel">
<h3>Overlay assets</h3> <h3>Overlay assets</h3>
<p>Upload overlay visuals and adjust them inline.</p> <p>Upload overlay visuals and adjust them inline.</p>
<div class="upload-row"> <div class="asset-management">
<input id="asset-file" class="file-input-field" type="file" accept="image/*,video/*,audio/*" onchange="handleFileSelection(this)" /> <div class="asset-column">
<label for="asset-file" class="file-input-trigger"> <div class="upload-row">
<span class="file-input-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span> <input id="asset-file" class="file-input-field" type="file" accept="image/*,video/*,audio/*" onchange="handleFileSelection(this)" />
<span class="file-input-copy"> <label for="asset-file" class="file-input-trigger">
<strong>Select an image, GIF, video, or audio</strong> <span class="file-input-icon"><i class="fa-solid fa-cloud-arrow-up"></i></span>
<small id="asset-file-name">No file chosen</small> <span class="file-input-copy">
</span> <strong>Select an image, GIF, video, or audio</strong>
</label> <small id="asset-file-name">No file chosen</small>
<button onclick="uploadAsset()">Upload</button> </span>
</div> </label>
<ul id="asset-list" class="asset-list"></ul> <button onclick="uploadAsset()">Upload</button>
<div id="asset-controls-placeholder" class="hidden"></div>
<div id="asset-controls" class="hidden asset-settings">
<div class="panel-section">
<div class="section-header">
<h5>Layout & order</h5>
</div> </div>
<div class="control-grid condensed three-col"> <ul id="asset-list" class="asset-list"></ul>
<label> </div>
Width <div class="asset-column inspector">
<input id="asset-width" class="number-input" type="number" min="10" step="5" /> <div id="asset-inspector" class="asset-inspector panel-section hidden">
</label> <div class="selected-asset-banner">
<label> <div class="selected-asset-main">
Height <div class="title-row">
<input id="asset-height" class="number-input" type="number" min="10" step="5" /> <strong id="selected-asset-name">Choose an asset</strong>
</label> </div>
<label class="checkbox-inline"> <p class="meta-text" id="selected-asset-meta">Pick an asset in the list to adjust its placement and playback.</p>
<input id="maintain-aspect" type="checkbox" checked /> <div class="badge-row asset-meta-badges" id="selected-asset-badges"></div>
Maintain aspect
</label>
</div>
<div class="control-grid condensed">
<label>
Layer (Z)
<div class="badge-row stacked">
<span class="badge">Layer <strong id="asset-z-level">1</strong></span>
</div> </div>
</label> <div class="selected-asset-actions">
</div> <button type="button" class="ghost icon-button" id="selected-asset-visibility" title="Toggle visibility">
<div class="control-actions filled compact"> <i class="fa-solid fa-eye-slash"></i><span class="sr-only">Toggle visibility</span>
<button type="button" onclick="sendToBack()" class="secondary" title="Send to back"><i class="fa-solid fa-angles-down"></i></button> </button>
<button type="button" onclick="bringBackward()" class="secondary" title="Move backward"><i class="fa-solid fa-arrow-down"></i></button> <button type="button" class="ghost danger icon-button" id="selected-asset-delete" title="Delete asset">
<button type="button" onclick="bringForward()" class="secondary" title="Move forward"><i class="fa-solid fa-arrow-up"></i></button> <i class="fa-solid fa-trash"></i><span class="sr-only">Delete asset</span>
<button type="button" onclick="bringToFront()" class="secondary" title="Bring to front"><i class="fa-solid fa-angles-up"></i></button> </button>
<button type="button" onclick="recenterSelectedAsset()" class="secondary" title="Center on canvas"><i class="fa-solid fa-bullseye"></i></button> </div>
<button type="button" onclick="nudgeRotation(-5)" class="secondary" title="Rotate left"><i class="fa-solid fa-rotate-left"></i></button> </div>
<button type="button" onclick="nudgeRotation(5)" class="secondary" title="Rotate right"><i class="fa-solid fa-rotate-right"></i></button> <div id="asset-controls-placeholder" class="asset-controls-placeholder">
</div> <div id="asset-controls" class="hidden asset-settings">
</div> <div class="panel-section">
<div class="section-header">
<h5>Layout & order</h5>
</div>
<div class="control-grid condensed three-col">
<label>
Width
<input id="asset-width" class="number-input" type="number" min="10" step="5" />
</label>
<label>
Height
<input id="asset-height" class="number-input" type="number" min="10" step="5" />
</label>
<label class="checkbox-inline">
<input id="maintain-aspect" type="checkbox" checked />
Maintain aspect
</label>
</div>
<div class="control-grid condensed">
<label>
Layer (Z)
<div class="badge-row stacked">
<span class="badge">Layer <strong id="asset-z-level">1</strong></span>
</div>
</label>
</div>
<div class="control-actions filled compact">
<button type="button" onclick="sendToBack()" class="secondary" title="Send to back"><i class="fa-solid fa-angles-down"></i></button>
<button type="button" onclick="bringBackward()" class="secondary" title="Move backward"><i class="fa-solid fa-arrow-down"></i></button>
<button type="button" onclick="bringForward()" class="secondary" title="Move forward"><i class="fa-solid fa-arrow-up"></i></button>
<button type="button" onclick="bringToFront()" class="secondary" title="Bring to front"><i class="fa-solid fa-angles-up"></i></button>
<button type="button" onclick="recenterSelectedAsset()" class="secondary" title="Center on canvas"><i class="fa-solid fa-bullseye"></i></button>
<button type="button" onclick="nudgeRotation(-5)" class="secondary" title="Rotate left"><i class="fa-solid fa-rotate-left"></i></button>
<button type="button" onclick="nudgeRotation(5)" class="secondary" title="Rotate right"><i class="fa-solid fa-rotate-right"></i></button>
</div>
</div>
<div class="panel-section" id="playback-section"> <div class="panel-section" id="playback-section">
<div class="section-header"> <div class="section-header">
<h5>Playback</h5> <h5>Playback</h5>
</div> </div>
<div class="control-grid condensed"> <div class="control-grid condensed">
<label> <label>
Animation speed Animation speed
<input id="asset-speed" class="range-input" type="range" min="0" max="1000" step="10" value="100" /> <input id="asset-speed" class="range-input" type="range" min="0" max="1000" step="10" value="100" />
</label> </label>
<div class="range-meta"><span>0%</span><span>1000%</span></div> <div class="range-meta"><span>0%</span><span>1000%</span></div>
<label class="checkbox-inline"> <label class="checkbox-inline">
<input id="asset-muted" type="checkbox" /> <input id="asset-muted" type="checkbox" />
Mute Mute
</label> </label>
</div> </div>
</div> </div>
<div class="panel-section hidden" id="audio-section"> <div class="panel-section hidden" id="audio-section">
<div class="section-header"> <div class="section-header">
<h5>Audio</h5> <h5>Audio</h5>
</div> </div>
<div class="control-grid condensed three-col"> <div class="control-grid condensed three-col">
<label class="checkbox-inline"> <label class="checkbox-inline">
<input id="asset-audio-loop" type="checkbox" /> <input id="asset-audio-loop" type="checkbox" />
Loop Loop
</label> </label>
<label> <label>
Delay (ms) Delay (ms)
<input id="asset-audio-delay" class="number-input" type="number" min="0" step="100" /> <input id="asset-audio-delay" class="number-input" type="number" min="0" step="100" />
</label> </label>
<label> <label>
Pitch (%) Pitch (%)
<input id="asset-audio-pitch" class="range-input" type="range" min="50" max="200" step="5" value="100" /> <input id="asset-audio-pitch" class="range-input" type="range" min="50" max="200" step="5" value="100" />
</label> </label>
<label> <label>
Volume (%) Volume (%)
<input id="asset-audio-volume" class="range-input" type="range" min="0" max="100" step="1" value="100" /> <input id="asset-audio-volume" class="range-input" type="range" min="0" max="100" step="1" value="100" />
</label> </label>
</div> </div>
<div class="control-grid condensed"> <div class="control-grid condensed">
<label> <label>
Playback speed Playback speed
<input id="asset-audio-speed" class="range-input" type="range" min="25" max="400" step="5" value="100" /> <input id="asset-audio-speed" class="range-input" type="range" min="25" max="400" step="5" value="100" />
</label> </label>
<div class="range-meta"><span>0.25x</span><span>4x</span></div> <div class="range-meta"><span>0.25x</span><span>4x</span></div>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>