updates UI

This commit is contained in:
mike
2026-06-29 12:56:31 +02:00
parent 150ef6dab0
commit 139785d108
4 changed files with 262 additions and 58 deletions

View File

@@ -2334,8 +2334,13 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"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",
"20260624_154536_img_72.png",
"20260629_114451_sc_sc_image.nobg.png",
"20260629_114346_sc_sc_image.nobg.png",
"_turntable/cg_4e2b6449/views/view_023_345deg.png",
@@ -5710,6 +5715,7 @@
"20260624_154712_img_72.png",
"20260624_154639_img_72.png",
"20260624_154548_img_72.png",
"20260624_154536_img_72.png",
"20260624_154525_img_72.png",
"20260624_154514_img_72.png",
"20260624_154503_img_72.png",
@@ -12806,7 +12812,8 @@
}
if (!data) {
try {
const r = await fetch(`${API}/videos`);
const url = forceFresh ? `${API}/videos?refresh=true` : `${API}/videos`;
const r = await fetch(url);
if (r.ok) { data = await r.json(); }
} catch (_) {}
}
@@ -12871,16 +12878,89 @@
}
// Template grid view
let html = '<div class="sb-label">Template videos</div>';
if (!availableVideos.length) {
let html = `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<div class="sb-label" style="margin:0">Template videos</div>
<button class="sb-btn" onclick="refreshTemplateVideos(event)" style="font-size:10px;padding:2px 6px;line-height:1">🔄 Refresh</button>
</div>`;
if (!wireframeGroups || !wireframeGroups.length) {
html += '<div style="font-size:11px;color:#555;padding:6px 0">No wireframe videos found</div>';
} else {
html += '<div class="sb-template-grid">';
availableVideos.forEach(v => {
const vSafe = v.replace(/'/g, "\\'");
const trimBtn = `<button class="sb-template-trim-btn" title="Trim video"
onclick="event.stopPropagation();openTrimPanel('${vSafe}')">✂</button>`;
html += _tplCardHTML(v, _fsSelectedVideo === v, `sbSelectTemplate('${vSafe}')`, trimBtn);
html += '<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">';
if (!window._expandedVideoGroups) {
window._expandedVideoGroups = {};
}
wireframeGroups.forEach(g => {
const hasClips = g.clips && g.clips.length > 0;
const repVideo = g.video || (hasClips ? g.clips[0] : null);
if (!repVideo) return;
const isExpanded = !!window._expandedVideoGroups[g.stem];
const repVideoSafe = repVideo.replace(/'/g, "\\'");
const isSelected = _fsSelectedVideo === repVideo || (g.video && _fsSelectedVideo === g.video);
const selectedClip = hasClips ? g.clips.find(clip => _fsSelectedVideo === clip) : null;
const rowBorder = (isSelected || selectedClip) ? 'border-color:#6d28d9;background:#1e1b4b' : 'border-color:#27272a;background:#09090b';
html += `
<div style="border:1px solid;border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:8px;transition:all 0.15s;${rowBorder}">
<div style="display:flex;align-items:center;gap:10px">
<div style="width:70px;height:50px;flex-shrink:0;position:relative;border-radius:4px;overflow:hidden;background:#000">
<div class="sb-template-card compact-card ${isSelected && !selectedClip ? 'selected' : ''}"
onclick="sbSelectTemplate('${repVideoSafe}')"
onmouseenter="_tplPlay(this,'${repVideoSafe}')" onmouseleave="_tplStop(this)"
style="width:100%;height:100%;margin:0;border:none">
<img src="${API}/wireframe/frame/${encodeURIComponent(repVideo)}?t=0" loading="lazy" style="width:100%;height:100%;object-fit:cover" alt="">
</div>
</div>
<div style="flex-grow:1;min-width:0">
<div style="font-size:12px;font-weight:bold;color:#f4f4f5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${escHtml(g.stem)}">
📹 ${escHtml(g.stem)}
</div>
<div style="font-size:10px;color:#71717a;margin-top:2px;display:flex;align-items:center;gap:4px">
<span>${g.video ? 'Full video' : 'Clips only'}</span>
${selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected clip: ${escHtml(selectedClip.replace(g.stem + '_', '').replace('.mp4', ''))}</span>` : ''}
${isSelected && !selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected</span>` : ''}
</div>
</div>
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
${g.video ? `<button class="sb-template-trim-btn" title="Trim video" onclick="event.stopPropagation();openTrimPanel('${repVideoSafe}')" style="position:static;width:24px;height:24px;line-height:24px;font-size:11px;padding:0">✂</button>` : ''}
${hasClips ? `
<button class="sb-btn" onclick="event.stopPropagation();toggleGroupClips('${g.stem.replace(/'/g, "\\'")}')"
style="font-size:10px;padding:3px 6px;line-height:1;background:#27272a;color:#d4d4d8;border:1px solid #3f3f46;display:flex;align-items:center;gap:2px">
<span>Clips (${g.clips.length})</span>
<span>${isExpanded ? '▲' : '▼'}</span>
</button>
` : ''}
</div>
</div>`;
if (hasClips && isExpanded) {
html += `
<div style="border-top:1px solid #27272a;padding-top:6px;margin-top:2px">
<div style="font-size:9px;color:#71717a;text-transform:uppercase;font-weight:bold;letter-spacing:0.5px;margin-bottom:4px">Sub-Clips</div>
<div style="display:flex;flex-wrap:wrap;gap:4px">`;
g.clips.forEach(clip => {
const cSafe = clip.replace(/'/g, "\\'");
const isClipSelected = _fsSelectedVideo === clip;
const clipLabel = clip.replace(g.stem + '_', '').replace('.mp4', '');
const pillBg = isClipSelected ? 'background:#6d28d9;color:#fff;border-color:#8b5cf6' : 'background:#18181b;color:#a1a1aa;border-color:#27272a';
html += `
<button class="sb-btn" onclick="sbSelectTemplate('${cSafe}')"
style="font-size:10px;padding:2px 6px;border-radius:3px;border:1px solid;cursor:pointer;transition:all 0.1s;${pillBg}">
${escHtml(clipLabel)}
</button>`;
});
html += `
</div>
</div>`;
}
html += `</div>`;
});
html += '</div>';
}
@@ -12937,6 +13017,28 @@
renderSidebarFaceswap();
}
function toggleGroupClips(stem) {
if (!window._expandedVideoGroups) window._expandedVideoGroups = {};
window._expandedVideoGroups[stem] = !window._expandedVideoGroups[stem];
if (_activeSidebarTab === 'faceswap') {
renderSidebarFaceswap();
} else if (_activeSidebarTab === 'scenery') {
renderSidebarScenery();
}
}
async function refreshTemplateVideos(event) {
if (event) event.stopPropagation();
showToast("Refreshing video library...", "info");
await loadTemplateVideos(true);
if (_activeSidebarTab === 'faceswap') {
renderSidebarFaceswap();
} else if (_activeSidebarTab === 'scenery') {
renderSidebarScenery();
}
showToast("Video library refreshed", "success");
}
function openTrimPanel(videoName) {
_fsTrimVideo = videoName;
renderSidebarFaceswap();
@@ -13275,56 +13377,118 @@
// --- Video picker grid (collapsible) ---
if (!_sceneVideo || gridOpen) {
html += '<div class="sb-label">Background source library</div>';
html += `
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">
<div class="sb-label" style="margin:0">Background source library</div>
<button class="sb-btn" onclick="refreshTemplateVideos(event)" style="font-size:10px;padding:2px 6px;line-height:1">🔄 Refresh</button>
</div>`;
if (wireframeGroups && wireframeGroups.length) {
html += '<div style="display:flex;flex-direction:column;gap:12px;margin-bottom:12px">';
html += '<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:12px">';
if (!window._expandedVideoGroups) {
window._expandedVideoGroups = {};
}
wireframeGroups.forEach(g => {
const hasClips = g.clips && g.clips.length;
const hasFrames = g.frames && g.frames.length;
const hasClips = g.clips && g.clips.length > 0;
const hasFrames = g.frames && g.frames.length > 0;
const repVideo = g.video || (hasClips ? g.clips[0] : null);
if (!repVideo) return;
const isExpanded = !!window._expandedVideoGroups[g.stem];
const repVideoSafe = repVideo.replace(/'/g, "\\'");
const isSelected = _sceneVideo === repVideo || (g.video && _sceneVideo === g.video);
const selectedClip = hasClips ? g.clips.find(clip => _sceneVideo === clip) : null;
const selectedFrame = hasFrames ? g.frames.find(frame => _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame) : null;
const rowBorder = (isSelected || selectedClip || selectedFrame) ? 'border-color:#6d28d9;background:#1e1b4b' : 'border-color:#27272a;background:#09090b';
html += `
<div style="background:#141414;border:1px solid #222;border-radius:6px;padding:8px">
<div style="font-size:12px;font-weight:bold;color:#ccc;margin-bottom:6px;display:flex;align-items:center;justify-content:space-between">
<span>📹 ${escHtml(g.stem)}</span>
<span style="font-size:10px;color:#666">${g.video ? 'source video' : 'no source'}</span>
<div style="border:1px solid;border-radius:6px;padding:8px;display:flex;flex-direction:column;gap:8px;transition:all 0.15s;${rowBorder}">
<div style="display:flex;align-items:center;gap:10px">
<div style="width:70px;height:50px;flex-shrink:0;position:relative;border-radius:4px;overflow:hidden;background:#000">
<div class="sb-template-card compact-card ${isSelected && !selectedClip ? 'selected' : ''}"
onclick="sceneSelectVideo('${repVideoSafe}')"
onmouseenter="_tplPlay(this,'${repVideoSafe}')" onmouseleave="_tplStop(this)"
style="width:100%;height:100%;margin:0;border:none">
<img src="${API}/wireframe/frame/${encodeURIComponent(repVideo)}?t=0" loading="lazy" style="width:100%;height:100%;object-fit:cover" alt="">
</div>
</div>
<div style="flex-grow:1;min-width:0">
<div style="font-size:12px;font-weight:bold;color:#f4f4f5;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${escHtml(g.stem)}">
📹 ${escHtml(g.stem)}
</div>
<div style="font-size:10px;color:#71717a;margin-top:2px;display:flex;align-items:center;gap:4px">
<span>${g.video ? 'Full video' : 'Clips only'}</span>
${selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected clip: ${escHtml(selectedClip.replace(g.stem + '_', '').replace('.mp4', ''))}</span>` : ''}
${selectedFrame ? `<span style="color:#a78bfa;font-weight:bold">· Selected frame</span>` : ''}
${isSelected && !selectedClip ? `<span style="color:#a78bfa;font-weight:bold">· Selected</span>` : ''}
</div>
</div>
<div style="display:flex;align-items:center;gap:4px;flex-shrink:0">
${(hasClips || hasFrames) ? `
<button class="sb-btn" onclick="event.stopPropagation();toggleGroupClips('${g.stem.replace(/'/g, "\\'")}')"
style="font-size:10px;padding:3px 6px;line-height:1;background:#27272a;color:#d4d4d8;border:1px solid #3f3f46;display:flex;align-items:center;gap:2px">
<span>Clips/Frames</span>
<span>${isExpanded ? '▲' : '▼'}</span>
</button>
` : ''}
</div>
</div>`;
// Render main video if exists
if (g.video) {
html += '<div class="sb-template-grid" style="margin-bottom:6px">';
const vSafe = g.video.replace(/'/g, "\\'");
html += _tplCardHTML(g.video, _sceneVideo === g.video, `sceneSelectVideo('${vSafe}')`);
html += '</div>';
}
// Render trimmed clips if they exist
if (hasClips) {
html += '<div style="font-size:10px;color:#888;margin:4px 0 2px">✂ Trimmed Clips:</div>';
html += '<div class="sb-template-grid" style="margin-bottom:6px">';
g.clips.forEach(clip => {
const cSafe = clip.replace(/'/g, "\\'");
html += _tplCardHTML(clip, _sceneVideo === clip, `sceneSelectVideo('${cSafe}')`);
});
html += '</div>';
}
// Render extracted frames if they exist
if (hasFrames) {
html += '<div style="font-size:10px;color:#888;margin:4px 0 2px">🖼 Extracted Frames:</div>';
html += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
g.frames.forEach(frame => {
const fSafe = frame.replace(/'/g, "\\'");
const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame;
const borderStyle = isSelected ? 'border:2px solid #2563eb' : 'border:1px solid #333';
if ((hasClips || hasFrames) && isExpanded) {
html += `
<div style="border-top:1px solid #27272a;padding-top:6px;margin-top:2px;display:flex;flex-direction:column;gap:6px">`;
if (hasClips) {
html += `
<div style="cursor:pointer;position:relative" title="${escHtml(frame)}" onclick="sceneSelectWireframeImage('${fSafe}')">
<img src="${API}/wireframe/${encodeURIComponent(frame)}" style="width:48px;height:48px;object-fit:cover;border-radius:4px;${borderStyle}">
<div>
<div style="font-size:9px;color:#71717a;text-transform:uppercase;font-weight:bold;letter-spacing:0.5px;margin-bottom:4px">Sub-Clips</div>
<div style="display:flex;flex-wrap:wrap;gap:4px">`;
g.clips.forEach(clip => {
const cSafe = clip.replace(/'/g, "\\'");
const isClipSelected = _sceneVideo === clip;
const clipLabel = clip.replace(g.stem + '_', '').replace('.mp4', '');
const pillBg = isClipSelected ? 'background:#6d28d9;color:#fff;border-color:#8b5cf6' : 'background:#18181b;color:#a1a1aa;border-color:#27272a';
html += `
<button class="sb-btn" onclick="sceneSelectVideo('${cSafe}')"
style="font-size:10px;padding:2px 6px;border-radius:3px;border:1px solid;cursor:pointer;transition:all 0.1s;${pillBg}">
${escHtml(clipLabel)}
</button>`;
});
html += `
</div>
</div>`;
});
html += '</div>';
}
if (hasFrames) {
html += `
<div>
<div style="font-size:9px;color:#71717a;text-transform:uppercase;font-weight:bold;letter-spacing:0.5px;margin-bottom:4px">Extracted Frames</div>
<div style="display:flex;flex-wrap:wrap;gap:4px">`;
g.frames.forEach(frame => {
const fSafe = frame.replace(/'/g, "\\'");
const isSelected = _sceneRefs[0] && _sceneRefs[0].kind === 'wireframe' && _sceneRefs[0].name === frame;
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}">
</div>`;
});
html += `
</div>
</div>`;
}
html += `
</div>`;
}
html += '</div>';
html += `</div>`;
});
html += '</div>';
} else if (availableVideos.length) {