diff --git a/src/main/resources/static/css/styles.css b/src/main/resources/static/css/styles.css index 0d5f9a0..41f71f4 100644 --- a/src/main/resources/static/css/styles.css +++ b/src/main/resources/static/css/styles.css @@ -408,6 +408,35 @@ body { 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 { list-style: none; padding: 0; @@ -503,6 +532,15 @@ body { 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 { display: grid; grid-template-columns: 1fr auto; @@ -680,6 +718,11 @@ body { gap: 8px; } +.asset-inspector .selected-asset-actions .icon-button, +.asset-inspector .selected-asset-actions .icon-button:disabled { + background: #0f172a; +} + .asset-meta-badges { margin-top: 4px; } diff --git a/src/main/resources/static/js/admin.js b/src/main/resources/static/js/admin.js index e44786e..c26fe6e 100644 --- a/src/main/resources/static/js/admin.js +++ b/src/main/resources/static/js/admin.js @@ -36,6 +36,12 @@ const audioPitchInput = document.getElementById('asset-audio-pitch'); const audioVolumeInput = document.getElementById('asset-audio-volume'); const controlsPlaceholder = document.getElementById('asset-controls-placeholder'); 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(''); const aspectLockState = new Map(); const commitSizeChange = debounce(() => applyTransformFromInputs(), 180); @@ -59,6 +65,20 @@ if (audioDelayInput) audioDelayInput.addEventListener('input', updateAudioSettin if (audioSpeedInput) audioSpeedInput.addEventListener('input', updateAudioSettingsFromInputs); if (audioPitchInput) audioPitchInput.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() { const socket = new SockJS('/ws'); stompClient = Stomp.over(socket); @@ -824,6 +844,9 @@ function renderAssetList() { if (!assets.size) { selectedAssetId = null; + if (assetInspector) { + assetInspector.classList.add('hidden'); + } const empty = document.createElement('li'); empty.textContent = 'No assets yet. Upload to get started.'; list.appendChild(empty); @@ -831,6 +854,10 @@ function renderAssetList() { return; } + if (assetInspector) { + assetInspector.classList.remove('hidden'); + } + const sortedAssets = getChronologicalAssets(); sortedAssets.forEach((asset) => { const li = document.createElement('li'); @@ -903,18 +930,10 @@ function renderAssetList() { }); 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); }); + + updateSelectedAssetControls(); } function createBadge(label, extraClass = '') { @@ -957,6 +976,12 @@ function getSelectedAsset() { } function updateSelectedAssetControls(asset = getSelectedAsset()) { + if (controlsPlaceholder && controlsPanel && controlsPanel.parentElement !== controlsPlaceholder) { + controlsPlaceholder.appendChild(controlsPanel); + } + + updateSelectedAssetSummary(asset); + if (!controlsPanel || !asset) { if (controlsPanel) controlsPanel.classList.add('hidden'); 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 + ? `` + : ''; + } + if (selectedDeleteBtn) { + selectedDeleteBtn.disabled = !asset; + selectedDeleteBtn.title = asset ? 'Delete asset' : 'Delete asset'; + } +} + function applyTransformFromInputs() { const asset = getSelectedAsset(); if (!asset) return; diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index ef4aa5b..9336be1 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -27,102 +27,128 @@
Upload overlay visuals and adjust them inline.
-