updates UI
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user