updates UI

This commit is contained in:
mike
2026-06-29 14:10:58 +02:00
parent 139785d108
commit 61268de34b
4 changed files with 394 additions and 65 deletions

View File

@@ -2334,12 +2334,24 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"20260619_040244_5_20260619_040135_image.png",
"cg_f85e7ae1_face.png",
"20260618_181804_8_20260618_181600_image.png",
"b1.png_face.png",
"_turntable/b1.png/views/view_023_345deg.nobg.png",
"cg_e6ba3260_face.png",
"cg_4e2b6449_face.png",
"cg_ad73335e_face.png",
"cg_e964fb19_face.png",
"20260629_133926_6_20260621_202521_image.png",
"out4.png_face.png",
"20260629_132244_sc_0_20260619_124038_image.png",
"20260629_132101_sc_0_20260619_124038_image.png",
"20260629_131834_sc_1_20260619_123958_image.png",
"20260629_124948_sc_view_023_345deg.png",
"20260629_124523_sc_view_023_345deg.png",
"20260629_124433_sc_view_023_345deg.png",
"20260629_122907_fs_Step Sis will do anything to make me Delete this Videos_1633s-2269s_image.png_prev50.mp4",
"20260629_123127_test_upload_image.png",
"20260629_123101_test_upload_image.png",
"20260629_120459_fs_bitch insemination_dup_20260626_003125_dup_Pasted image (9).png.mp4",
"20260629_114451_sc_sc_image.nobg.png",
"20260629_114346_sc_sc_image.nobg.png",
@@ -5364,7 +5376,6 @@
"20260626_005504_Pasted image (9).png",
"20260626_005322_dup_20260626_003125_dup_Pasted image (9).png",
"20260626_003125_dup_Pasted image (9).png",
"cg_ad73335e_face.png",
"20260625_232346_sc_image.png",
"20260625_232242_sc_image.png",
"20260625_232204_sc_image.png",
@@ -6162,8 +6173,8 @@
"20260622_193411_1_20260622_193118_image.png",
"20260622_193354_0_20260622_193118_image.png",
"20260622_193118_image.png",
"20260622_212012_dup_20260622_181910_0_20260619_124038_image.png",
"20260622_181910_0_20260619_124038_image.png",
"20260622_212012_dup_20260622_181910_0_20260619_124038_image.png",
"20260622_181734_0_20260619_124038_image.png",
"20260622_181720_0_20260619_124038_image.png",
"20260622_181634_0_20260619_124038_image.png",
@@ -6955,7 +6966,6 @@
"20260619_040319_8_20260619_040135_image.png",
"20260619_040307_7_20260619_040135_image.png",
"20260619_040255_6_20260619_040135_image.png",
"20260619_040244_5_20260619_040135_image.png",
"20260619_040233_4_20260619_040135_image.png",
"20260619_040221_3_20260619_040135_image.png",
"20260619_040135_image.png",
@@ -7048,7 +7058,6 @@
"20260618_181839_4_20260618_181715_image.png",
"20260618_181827_3_20260618_181715_image.png",
"20260618_181815_2_20260618_181715_image.png",
"20260618_181804_8_20260618_181600_image.png",
"20260618_181752_1_20260618_181715_image.png",
"20260618_181742_7_20260618_181600_image.png",
"20260618_181730_0_20260618_181715_image.png",
@@ -8676,10 +8685,31 @@
if (refsDiv) {
if (refs && refs.length > 0) {
refsDiv.style.display = 'flex';
refsDiv.innerHTML = refs.map(r =>
`<img src="${IMAGE_FOLDER + r}" loading="lazy" title="${r}"
onclick="jumpToImage('${r.replace(/'/g,"\\'")}');">`
).join('');
refsDiv.innerHTML = refs.map(r => {
let srcUrl = '';
let isGallery = true;
let titleText = r;
if (r.startsWith('video:')) {
const vidName = r.substring(6);
srcUrl = `${API}/wireframe/frame/${encodeURIComponent(vidName)}?t=0`;
isGallery = false;
titleText = `Scene Video: ${vidName}`;
} else if (r.startsWith('wireframe:')) {
const wfName = r.substring(10);
srcUrl = `${API}/wireframe/${encodeURIComponent(wfName)}`;
isGallery = false;
titleText = `Scene Image: ${wfName}`;
} else {
srcUrl = IMAGE_FOLDER + r;
}
const clickAction = isGallery
? `jumpToImage('${r.replace(/'/g,"\\'")}');`
: `showToast('${titleText.replace(/'/g,"\\'")}','info');`;
return `<img src="${srcUrl}" loading="lazy" decoding="async" title="${escHtml(titleText)}"
onerror="this.style.opacity=.3"
onclick="${clickAction}">`;
}).join('');
} else {
refsDiv.style.display = 'none';
}
@@ -11712,12 +11742,21 @@
}
}
if (e.altKey && e.key === 'Enter') {
if (document.getElementById('studio').classList.contains('open') && _activeSidebarTab === 'generate') {
const btn = document.getElementById('sbGenBtn');
if (btn && !btn.disabled) {
btn.click();
e.preventDefault();
return;
if (document.getElementById('studio').classList.contains('open')) {
if (_activeSidebarTab === 'generate') {
const btn = document.getElementById('sbGenBtn');
if (btn && !btn.disabled) {
btn.click();
e.preventDefault();
return;
}
} else if (_activeSidebarTab === 'scenery') {
const btn = document.getElementById('sceneGenBtn');
if (btn && !btn.disabled) {
btn.click();
e.preventDefault();
return;
}
}
}
}
@@ -13353,24 +13392,27 @@
// --- Selected video: player + slider + capture (always at top) ---
if (_sceneVideo) {
if (!gridOpen) {
html += `
<div class="scene-frame-preview" id="sceneFramePreview" style="display:block">
<video id="sceneVideoEl" muted preload="auto" playsinline
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;height:100%;object-fit:contain"></video>
<img id="sceneFrameImg" style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;height:100%;object-fit:contain"
${_sceneFrameBytes?`src="data:image/png;base64,${_sceneFrameBytes}"`:''}/>
<div class="scene-captured-badge" id="sceneCapturedBadge" style="${_sceneFrameBytes?'display:block':'display:none'}">✓ frame captured</div>
</div>
<input type="range" class="scene-scrubber" id="sceneScrubber" min="0" max="1000" value="0"
oninput="sceneScrub(this.value)" ${_sceneFrameBytes?'disabled':''}>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span id="sceneTimeLabel" style="font-size:11px;color:#888">0:00.0</span>
<span style="font-size:10px;color:#555">${escHtml(_sceneVideo.replace(/\.[^.]+$/,''))}</span>
</div>
<button class="sb-btn primary" id="sceneCaptureBtn" onclick="sceneExtractFrame()"
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;margin-bottom:8px">📷 Capture this frame</button>
<button class="sb-btn" id="sceneReleaseBtn" onclick="sceneReleaseFrame()"
style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;margin-bottom:8px">✖ Pick a different frame</button>`;
}
html += `
<div class="scene-frame-preview" id="sceneFramePreview" style="display:block">
<video id="sceneVideoEl" muted preload="auto" playsinline
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;height:100%;object-fit:contain"></video>
<img id="sceneFrameImg" style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;height:100%;object-fit:contain"
${_sceneFrameBytes?`src="data:image/png;base64,${_sceneFrameBytes}"`:''}/>
<div class="scene-captured-badge" id="sceneCapturedBadge" style="${_sceneFrameBytes?'display:block':'display:none'}">✓ frame captured</div>
</div>
<input type="range" class="scene-scrubber" id="sceneScrubber" min="0" max="1000" value="0"
oninput="sceneScrub(this.value)" ${_sceneFrameBytes?'disabled':''}>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<span id="sceneTimeLabel" style="font-size:11px;color:#888">0:00.0</span>
<span style="font-size:10px;color:#555">${escHtml(_sceneVideo.replace(/\.[^.]+$/,''))}</span>
</div>
<button class="sb-btn primary" id="sceneCaptureBtn" onclick="sceneExtractFrame()"
style="${_sceneFrameBytes?'display:none':'display:block'};width:100%;margin-bottom:8px">📷 Capture this frame</button>
<button class="sb-btn" id="sceneReleaseBtn" onclick="sceneReleaseFrame()"
style="${_sceneFrameBytes?'display:block':'display:none'};width:100%;margin-bottom:8px">✖ Pick a different frame</button>
<button class="sb-btn" onclick="sceneToggleGrid()" style="width:100%;margin-bottom:10px">
${gridOpen?'▲ Hide video list':'▼ Change background video'}</button>`;
}
@@ -13475,7 +13517,7 @@
const borderStyle = isSelected ? 'border:2px solid #8b5cf6' : 'border:1px solid #27272a';
html += `
<div style="cursor:pointer;position:relative" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(frame)}" style="width:40px;height:40px;object-fit:cover;border-radius:4px;${borderStyle}">
<img src="${API}/wireframe/${encodeURIComponent(frame)}" loading="lazy" decoding="async" style="width:40px;height:40px;object-fit:cover;border-radius:4px;${borderStyle}" onerror="this.style.opacity=.3">
</div>`;
});
@@ -13512,7 +13554,7 @@
const borderStyle = isSelected ? 'border:2px solid #2563eb' : 'border:1px solid #333';
html += `
<div style="cursor:pointer;position:relative" title="${escHtml(img)}" onclick="sceneSelectWireframeImage('${imgSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(img)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}">
<img src="${API}/wireframe/${encodeURIComponent(img)}" loading="lazy" decoding="async" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}" onerror="this.style.opacity=.3">
</div>`;
});
html += '</div>';
@@ -13608,26 +13650,51 @@
} else if (!_sceneLib.groups.length && !_sceneLib.ungrouped.length) {
html += `<div style="font-size:11px;color:#555;padding:4px 0">No previous scenery found.</div>`;
} else {
const renderLibItems = (items) => items.map(it => {
const url = IMAGE_FOLDER + encodeURIComponent(it.filename);
const gLabel = it.group_id ? ` <span style="color:#555">${escHtml(it.group_id)}</span>` : '';
return `<div style="display:inline-block;margin:2px;cursor:pointer;position:relative;vertical-align:top"
title="Use as background · ${escHtml(it.filename)}${it.group_id?' ('+escHtml(it.group_id)+')':''}"
onclick="sceneLibUseAsBackground(${JSON.stringify(it.filename)})">
<img src="${url}" style="width:52px;height:52px;object-fit:cover;border-radius:4px;border:1px solid #333">
</div>`;
}).join('');
if (!window._expandedSceneryGroups) {
window._expandedSceneryGroups = {};
}
const renderLibGroup = (groupKey, items) => {
const isExpanded = !!window._expandedSceneryGroups[groupKey];
const showCount = isExpanded ? items.length : 12;
const visibleItems = items.slice(0, showCount);
const hasMore = items.length > 12;
let groupHtml = `<div style="display:flex;flex-wrap:wrap;gap:4px">`;
groupHtml += visibleItems.map(it => {
const url = `${API}/output/` + encodeURIComponent(it.filename);
return `<div style="display:inline-block;cursor:pointer;position:relative;vertical-align:top"
title="Use as background · ${escHtml(it.filename)}${it.group_id?' ('+escHtml(it.group_id)+')':''}"
onclick="sceneLibUseAsBackground(${JSON.stringify(it.filename)})">
<img src="${url}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;border:1px solid #27272a;background:#09090b"
onerror="this.style.opacity=.3">
</div>`;
}).join('');
groupHtml += `</div>`;
if (hasMore) {
groupHtml += `
<div style="margin-top:4px">
<button class="sb-btn" onclick="toggleSceneryGroup('${groupKey.replace(/'/g, "\\'")}')"
style="font-size:10px;padding:2px 6px;line-height:1;background:#18181b;color:#a1a1aa;border-color:#27272a">
${isExpanded ? 'Show less' : `Show all (+${items.length - 12} more)`}
</button>
</div>`;
}
return groupHtml;
};
_sceneLib.groups.forEach(g => {
const vName = g.video.replace(/\.[^.]+$/, '');
html += `<div style="margin-bottom:8px">
<div style="font-size:10px;color:#888;margin-bottom:3px">📹 ${escHtml(vName)}</div>
<div>${renderLibItems(g.items)}</div>
const groupKey = 'group_' + g.video;
html += `<div style="margin-bottom:12px;background:#141414;border:1px solid #222;border-radius:6px;padding:8px">
<div style="font-size:11px;font-weight:bold;color:#ccc;margin-bottom:6px">📹 ${escHtml(vName)}</div>
${renderLibGroup(groupKey, g.items)}
</div>`;
});
if (_sceneLib.ungrouped.length) {
html += `<div style="margin-bottom:8px">
<div style="font-size:10px;color:#888;margin-bottom:3px">📷 uploaded / other</div>
<div>${renderLibItems(_sceneLib.ungrouped)}</div>
html += `<div style="margin-bottom:12px;background:#141414;border:1px solid #222;border-radius:6px;padding:8px">
<div style="font-size:11px;font-weight:bold;color:#ccc;margin-bottom:6px">📷 uploaded / other</div>
${renderLibGroup('ungrouped', _sceneLib.ungrouped)}
</div>`;
}
}
@@ -13649,18 +13716,20 @@
if (_sceneVideo) {
const vid = document.getElementById('sceneVideoEl');
vid.src = `${API}/wireframe/${encodeURIComponent(_sceneVideo)}`;
// Once metadata is known, size the slider so 1000 steps span the whole clip.
vid.addEventListener('loadedmetadata', () => {
_sceneDuration = vid.duration || 0;
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
}, { once: true });
// Keep the time label in sync if the frame is seeked any other way.
vid.addEventListener('seeked', () => {
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
});
if (vid) {
vid.src = `${API}/wireframe/${encodeURIComponent(_sceneVideo)}`;
// Once metadata is known, size the slider so 1000 steps span the whole clip.
vid.addEventListener('loadedmetadata', () => {
_sceneDuration = vid.duration || 0;
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
}, { once: true });
// Keep the time label in sync if the frame is seeked any other way.
vid.addEventListener('seeked', () => {
const lbl = document.getElementById('sceneTimeLabel');
if (lbl) lbl.textContent = formatSecs(vid.currentTime || 0);
});
}
}
panel.scrollTop = savedScrollTop;
@@ -13691,16 +13760,22 @@
renderSidebarScenery();
}
function toggleSceneryGroup(groupKey) {
if (!window._expandedSceneryGroups) window._expandedSceneryGroups = {};
window._expandedSceneryGroups[groupKey] = !window._expandedSceneryGroups[groupKey];
renderSidebarScenery();
}
// Use a library scenery image as slot 0 background (fetched to bytes).
async function sceneLibUseAsBackground(filename) {
try {
const url = IMAGE_FOLDER + encodeURIComponent(filename);
const url = `${API}/output/` + encodeURIComponent(filename);
const b64 = await _fetchAsBase64(url);
_sceneRefs[0] = { kind: 'bytes', data: b64, filename: filename };
_sceneVideo = null;
_sceneFrameBytes = null;
_sceneFrameBytes = b64; // Set this as well to align with other captured/set backgrounds!
_sceneGridOpen = false;
renderSidebarScenery();
_sceneRefresh();
showToast('Background set from previous scenery', 'success');
} catch (e) {
showToast('Could not load image: ' + e, 'error');
@@ -13724,6 +13799,9 @@
}
async function _fetchAsBase64(url) {
const r = await fetch(url);
if (!r.ok) {
throw new Error(`HTTP error! status: ${r.status}`);
}
const blob = await r.blob();
return await new Promise((res, rej) => {
const fr = new FileReader();