updates UI

This commit is contained in:
mike
2026-07-01 04:46:43 +02:00
parent 970daeba31
commit 251d9b1cc8
3 changed files with 463 additions and 80 deletions

View File

@@ -1709,21 +1709,72 @@
.sb-gen-refs .sb-gen-ref:hover .sb-gen-ref-x { display: flex; }
.sb-gen-refs .sb-gen-ref:hover img { border-color: #dc2626; }
#studioAngleBar {
position: absolute; bottom: 8px; left: 50%; transform: translateX(-50%);
display: flex; gap: 4px; opacity: 0; transition: opacity 0.2s;
z-index: 10; pointer-events: none;
position: absolute;
bottom: 12px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 6px;
opacity: 0;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
z-index: 100;
pointer-events: none;
background: rgba(10, 10, 10, 0.88);
border: 1px solid rgba(255, 255, 255, 0.08);
padding: 6px 12px;
border-radius: 30px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
backdrop-filter: blur(10px);
}
.studio-viewer:hover #studioAngleBar {
opacity: 1;
transform: translateX(-50%) translateY(-2px);
pointer-events: all;
}
.studio-viewer:hover #studioAngleBar { opacity: 1; pointer-events: all; }
#studioAngleBar button {
font-size: 15px; background: rgba(0,0,0,0.65); border: 1px solid rgba(255,255,255,0.18);
border-radius: 4px; color: #fff; width: 34px; height: 34px; cursor: pointer; line-height: 1;
transition: background 0.12s; position: relative;
font-size: 13px;
font-weight: 500;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 50%;
color: #ccc;
width: 32px;
height: 32px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
outline: none;
}
#studioAngleBar button:hover {
background: rgba(255, 255, 255, 0.12);
border-color: rgba(255, 255, 255, 0.18);
color: #fff;
transform: translateY(-1px);
}
#studioAngleBar button:active {
transform: translateY(1px);
}
#studioAngleBar button:hover { background: rgba(255,255,255,0.18); }
#studioAngleBar button[title]:hover::after {
content: attr(title); position: absolute; bottom: 38px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.8); color: #fff; font-size: 10px; padding: 2px 6px;
border-radius: 3px; white-space: nowrap; pointer-events: none;
content: attr(title);
position: absolute;
bottom: 42px;
left: 50%;
transform: translateX(-50%);
background: #111;
border: 1px solid #333;
color: #eee;
font-size: 10px;
padding: 4px 8px;
border-radius: 4px;
white-space: nowrap;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
z-index: 1000;
}
/* template grid (faceswap / scenery) */
@@ -1886,7 +1937,7 @@
</head>
<body>
<!-- Floating services status container -->
<div class="services-status-container" style="position: fixed; top: 12px; right: 12px; z-index: 11000; display: flex; align-items: center; gap: 14px; background: rgba(18, 18, 18, 0.9); border: 1px solid #2e2e2e; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 11px; color: #aaa; box-shadow: 0 4px 12px rgba(0,0,0,0.5); backdrop-filter: blur(4px);">
<div class="services-status-container" style="position: fixed; bottom: 12px; left: 12px; z-index: 11000; display: flex; align-items: center; gap: 14px; background: rgba(18, 18, 18, 0.9); border: 1px solid #2e2e2e; padding: 6px 12px; border-radius: 20px; font-family: monospace; font-size: 11px; color: #aaa; box-shadow: 0 4px 12px rgba(0,0,0,0.5); backdrop-filter: blur(4px);">
<div style="display: flex; align-items: center; gap: 6px; cursor: help;" id="backendStatus" title="Backend: Offline (API server is down)">
<span class="status-dot-led offline" id="backendDot"></span>
<span>API</span>
@@ -2068,7 +2119,8 @@
<span id="lbImgDims" style="font-size:11px;color:#444;margin:0 8px"></span>
<span id="lbCounter" style="font-size:12px;color:#555"></span>
</div>
<div class="studio-topbar-right">
<div class="studio-topbar-right" style="display:flex;align-items:center;gap:6px">
<button class="btn" id="studioPrivacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)" style="font-size:12px;padding:4px 8px">🔓</button>
<button class="btn" id="sidebarInnerToggle" onclick="toggleSidebar()" title="Collapse/expand toolbox"></button>
</div>
</div>
@@ -2144,13 +2196,23 @@
<div class="sb-panels">
<!-- Info panel: static HTML with existing lb* IDs so updateStudio() just works -->
<div class="sb-panel active" id="sbPanelInfo">
<div id="lbGenPromptWrap" style="display:none;margin-bottom:8px">
<!-- Pose-directed Prompt -->
<div id="lbGenPromptWrap" style="display:none;margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px">
<div class="sb-label" style="margin:0">Generation prompt</div>
<button class="sb-btn" onclick="suggestUpdatedDescription()" style="font-size:9px;padding:2px 6px;margin:0;min-width:auto;height:18px;line-height:1" title="Use AI to suggest an updated description from this image (Shortcut: R)">✨ Suggest Description</button>
<div class="sb-label" style="margin:0">Pose prompt (Actor-agnostic)</div>
<button class="sb-btn" onclick="suggestUpdatedDescription()" style="font-size:9px;padding:2px 6px;margin:0;min-width:auto;height:18px;line-height:1" title="Use AI to suggest updated pose prompt and character description (Shortcut: R)">✨ Suggest Details</button>
</div>
<div id="lbGenPrompt" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word"></div>
<div id="lbGenPrompt" class="editable-meta-field" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word;background:#141414;border:1px solid #2a2a2a;padding:6px;border-radius:4px;cursor:pointer" onclick="editMetadataField('prompt')">Click to add/edit pose prompt...</div>
</div>
<!-- Character & Scenery Description -->
<div id="lbDescriptionWrap" style="display:none;margin-bottom:12px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:3px">
<div class="sb-label" style="margin:0">Character & Scenery Description</div>
</div>
<div id="lbDescription" class="editable-meta-field" style="font-size:11px;color:#888;line-height:1.4;word-break:break-word;background:#141414;border:1px solid #2a2a2a;padding:6px;border-radius:4px;cursor:pointer" onclick="editMetadataField('description')">Click to add/edit description...</div>
</div>
<div id="lbSourceRefsWrap" style="display:none;margin-bottom:8px">
<div class="sb-label" style="margin-bottom:3px">Reference Images</div>
<div id="lbSourceRefs" class="sb-source-refs"></div>
@@ -2231,32 +2293,51 @@
</div>
<div class="sb-sep"></div>
<div class="sb-label">Actions</div>
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger">Re-tag</button>
<button class="sb-btn purple" id="lbUndressBtn" style="display:none" onclick="lbUndress()">Undress</button>
<!-- Geometry & Retouching Tools -->
<div style="font-size:10px;color:#666;margin-bottom:4px">Geometry & Retouching</div>
<div class="sb-actions" style="margin-bottom:8px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<button class="sb-btn" id="lbPoseBtn" onclick="lbTogglePose()" title="Preview body pose skeleton" style="margin:0; width:100%">Pose</button>
<button class="sb-btn" id="lbInvertAlphaBtn" onclick="lbInvertAlpha()" title="Invert transparency — fixes when the wrong segment was kept" style="margin:0; width:100%">Invert α</button>
<button class="sb-btn" id="lbRotateLeftBtn" onclick="lbRotate(-90)" title="Rotate 90° counter-clockwise" style="margin:0; width:100%">⟲ Rotate L</button>
<button class="sb-btn" id="lbRotateRightBtn" onclick="lbRotate(90)" title="Rotate 90° clockwise" style="margin:0; width:100%">⟳ Rotate R</button>
</div>
<div class="sb-actions" style="margin-bottom:8px">
<button class="sb-btn" id="lbExtract" style="display:none" onclick="extractCurrentImage()">Extract</button>
<button class="sb-btn" id="lbInvertAlphaBtn" onclick="lbInvertAlpha()" title="Invert transparency — fixes when the wrong segment was kept">Invert α</button>
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group">Duplicate</button>
<button class="sb-btn" id="lbRotateLeftBtn" onclick="lbRotate(-90)" title="Rotate 90° counter-clockwise">⟲ 90°</button>
<button class="sb-btn" id="lbRotateRightBtn" onclick="lbRotate(90)" title="Rotate 90° clockwise">⟳ 90°</button>
<button class="sb-btn" id="lbPoseBtn" onclick="lbTogglePose()" title="Preview body pose skeleton">Pose</button>
<button class="sb-btn" id="lbMakeVideoBtn" onclick="lbMakeVideo()" title="Stitch group images (or Shift+Click selection) into a short MP4">Make video</button>
<button class="sb-btn amber" id="lbOrbitBtn" onclick="lbOrbit()" title="Generate orbit preview (Fast=depth-card, Quality=Qwen turntable)">Orbit 3D<span id="orbitBadge" class="orbit-badge" style="display:none"></span></button>
<a class="sb-btn" id="lbDownloadBtn" download style="text-decoration:none">Download</a>
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b" title="Move to archive (recoverable)">Archive</button>
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" title="Click once to arm, then again to confirm">Delete</button>
<!-- AI & Generative features -->
<div style="font-size:10px;color:#666;margin-bottom:4px">AI & Export</div>
<div class="sb-actions" style="margin-bottom:8px; display: grid; grid-template-columns: 1fr 1fr; gap: 4px;">
<button class="sb-btn" onclick="tagCurrentImage()" title="Re-run tagger" style="margin:0; width:100%">Re-tag</button>
<button class="sb-btn" id="lbDuplicateBtn" onclick="lbDuplicate()" title="Duplicate image into same group" style="margin:0; width:100%">Duplicate</button>
<button class="sb-btn purple" id="lbUndressBtn" style="display:none; margin:0; width:100%" onclick="lbUndress()">Undress</button>
<button class="sb-btn" id="lbExtract" style="display:none; margin:0; width:100%" onclick="extractCurrentImage()">Extract</button>
<button class="sb-btn" id="lbMakeVideoBtn" onclick="lbMakeVideo()" title="Stitch group images (or Shift+Click selection) into a short MP4" style="margin:0; width:100%">Make video</button>
<button class="sb-btn amber" id="lbOrbitBtn" onclick="lbOrbit()" title="Generate orbit preview (Fast=depth-card, Quality=Qwen turntable)" style="margin:0; width:100%">Orbit 3D<span id="orbitBadge" class="orbit-badge" style="display:none"></span></button>
</div>
<!-- File actions -->
<div style="font-size:10px;color:#666;margin-bottom:4px">File Management</div>
<div class="sb-actions" style="margin-bottom:8px; display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 4px;">
<a class="sb-btn" id="lbDownloadBtn" download style="text-decoration:none; margin:0; width:100%; text-align:center; padding: 4px 0">Download</a>
<button class="sb-btn" id="lbArchiveBtn" onclick="lbArchive()" style="color:#f59e0b; margin:0; width:100%" title="Move to archive (recoverable)">Archive</button>
<button class="sb-btn danger" id="lbDeleteBtn" onclick="lbDeleteArm()" style="margin:0; width:100%" title="Click once to arm, then again to confirm">Delete</button>
</div>
<div class="sb-sep"></div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px">
<div class="sb-label" style="margin:0">Image name</div>
<button class="sb-btn" onclick="copyRawFilename()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy raw filename to clipboard">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>Copy
</button>
<div style="display:flex;gap:4px">
<button class="sb-btn" onclick="copyRawFilename()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy raw filename to clipboard">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>Copy ID
</button>
<button class="sb-btn" onclick="copyFilepath()" style="padding:2px 6px;font-size:9px;min-width:auto;display:inline-flex;align-items:center;gap:4px" title="Copy absolute file path to clipboard">
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>Copy Path
</button>
</div>
</div>
<input type="text" class="sb-input" id="lbNameInput"
placeholder="Image name…"
@@ -2390,6 +2471,7 @@
<button class="btn" onclick="selectAll()">All</button>
<button class="btn" onclick="deselectAll()">None</button>
<button class="btn" id="groupBtn" onclick="groupSelected()">Group</button>
<button class="btn" id="openStudioBtn" onclick="openStudioForSelected()" style="display:none">Open Studio</button>
<button class="btn" id="multiRefBtn" onclick="processMultiRef()" style="display:none" title="Use selected (23) as combined references → single output">Multi-ref</button>
<span id="multiRefLegend" style="display:none;font-size:10px;color:#888;white-space:nowrap"></span>
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
@@ -2421,6 +2503,11 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"20260630_130454_sc_0_20260622_101128_image.nobg.png",
"20260630_131638_sc_2_20260622_101128_image.nobg.nobg.png",
"20260630_151502_sc_0_20260622_101128_image.nobg.png",
"20260630_132351_sc_2_20260622_101128_image.nobg.nobg.png",
"20260618_052559_2_20260618_052526_image.nobg.png",
"20260701_031014_4_20260618_052526_image.png",
"20260701_030956_4_20260618_052526_image.png",
"20260701_030939_4_20260618_052526_image.png",
@@ -2513,7 +2600,6 @@
"20260630_232932_2_20260618_052526_image.nobg.png",
"20260630_232800_2_20260618_052526_image.nobg.png",
"20260630_232800_2_20260618_052526_image.png",
"20260618_052559_2_20260618_052526_image.nobg.png",
"20260630_231721_sc_5_20260618_052526_image.png",
"20260630_163400_image.png",
"20260630_163247_3_20260628_205049_image.png",
@@ -2523,13 +2609,9 @@
"20260630_151554_sc_0_20260622_101128_image.png",
"20260630_151502_sc_0_20260622_101128_image.png",
"20260630_132351_sc_2_20260622_101128_image.nobg.png",
"20260630_132133_sc_2_20260622_101128_image.nobg.png",
"20260630_131910_sc_2_20260622_101128_image.nobg.png",
"20260630_131638_sc_2_20260622_101128_image.nobg.png",
"20260630_131402_sc_2_20260622_101128_image.nobg.png",
"20260630_130738_sc_2_20260622_101128_image.nobg.png",
"20260630_130601_sc_4_20260622_101128_image.png",
"20260630_130454_sc_0_20260622_101128_image.png",
"20260630_130350_sc_0_20260622_101128_image.png",
"20260630_130220_sc_0_20260622_101128_image.png",
"20260630_125808_sc_0_20260622_101128_image.png",
@@ -2913,7 +2995,6 @@
"20260629_060736_2_20260629_060652_image.png",
"20260629_060725_1_20260629_060652_image.png",
"20260629_060714_0_20260629_060652_image.png",
"20260629_060652_image.png",
"cg_fb1043d8_face.png",
"20260629_055242_image.png",
"_turntable/cg_2bbb563a/views/view_022_330deg.png",
@@ -5832,10 +5913,6 @@
"20260626_005504_Pasted image (9).png",
"20260626_005322_dup_20260626_003125_dup_Pasted image (9).png",
"20260626_003125_dup_Pasted image (9).png",
"20260625_232346_sc_image.png",
"20260625_232242_sc_image.png",
"20260625_232204_sc_image.png",
"20260625_231601_sc_image.png",
"20260625_225929_sc_image.png",
"20260625_223304_p12.png",
"20260625_210437_image.png",
@@ -7876,12 +7953,14 @@
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
let _prevGid = null; // for "Back" button in studio
let imageNames = {}; // filename → human name (from /names)
let fileFilepaths = {}; // filename → absolute path on disk
let clipDescriptions = {}; // filename → description
let customGroups = {}; // filename → groupId (from /groups)
let groupNames = {}; // group_id → group display name
let fileSortOrders = {}; // filename → sort_order (integer or null)
let filePoses = {}; // filename → pose name
let filePrompts = {}; // filename → generation prompt
let fileDescriptions = {}; // filename → custom description
let fileHidden = {}; // filename → boolean (hidden from cycling)
let fileArchived = {}; // filename → boolean
let filePoseDescription = {}; // filename → human readable string
@@ -8911,6 +8990,10 @@
}
function closeStudio() {
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
window.location.href = 'car.html';
return;
}
document.getElementById('studio').classList.remove('open');
document.body.style.overflow = '';
stopSlideshow();
@@ -9176,11 +9259,34 @@
const clipEl = document.getElementById('lbClipDesc');
if (clipEl) clipEl.textContent = clipDescriptions[fname] || '';
// Always show prompt and description wraps if fname exists, so user can edit/create even if empty
const genPromptWrap = document.getElementById('lbGenPromptWrap');
const genPromptEl = document.getElementById('lbGenPrompt');
const genPrompt = filePrompts[fname] || '';
if (genPromptWrap) genPromptWrap.style.display = genPrompt ? '' : 'none';
if (genPromptEl) genPromptEl.textContent = genPrompt;
if (genPromptWrap && genPromptEl) {
genPromptWrap.style.display = fname ? '' : 'none';
const promptVal = filePrompts[fname] || '';
// Don't overwrite if actively editing (has textarea)
if (!genPromptEl.querySelector('textarea')) {
genPromptEl.textContent = promptVal || 'Click to add/edit pose prompt...';
genPromptEl.style.fontStyle = promptVal ? '' : 'italic';
genPromptEl.style.color = promptVal ? '' : '#555';
genPromptEl.onclick = () => editMetadataField('prompt');
}
}
const descWrap = document.getElementById('lbDescriptionWrap');
const descEl = document.getElementById('lbDescription');
if (descWrap && descEl) {
descWrap.style.display = fname ? '' : 'none';
const descVal = fileDescriptions[fname] || '';
// Don't overwrite if actively editing (has textarea)
if (!descEl.querySelector('textarea')) {
descEl.textContent = descVal || 'Click to add/edit description...';
descEl.style.fontStyle = descVal ? '' : 'italic';
descEl.style.color = descVal ? '' : '#555';
descEl.onclick = () => editMetadataField('description');
}
}
const pose = filePoses[fname];
const poseTag = document.getElementById('lbPoseTag');
@@ -10874,6 +10980,21 @@
});
}
function copyFilepath() {
const fname = lbNames[lbIdx];
if (!fname) return;
const filepath = fileFilepaths[fname] || '';
if (!filepath) {
showToast('File path not available', 'info');
return;
}
navigator.clipboard.writeText(filepath).then(() => {
showToast('Absolute file path copied to clipboard', 'success');
}).catch(err => {
showToast('Failed to copy: ' + err, 'error');
});
}
async function lbRemoveBg() {
if (!_backendOnline) {
showToast("Cannot remove background: Backend API is offline", "error");
@@ -11350,6 +11471,13 @@
if (!r.ok) { panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>'; return; }
const { turntables } = await r.json();
_turntablesData = turntables || [];
// Filter by LOAD_ONLY_GROUP_ID or window._selectedOrbitGroupIds
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
_turntablesData = _turntablesData.filter(t => t.group_id === LOAD_ONLY_GROUP_ID);
} else if (window._selectedOrbitGroupIds && window._selectedOrbitGroupIds.length > 0) {
_turntablesData = _turntablesData.filter(t => window._selectedOrbitGroupIds.includes(t.group_id));
}
} catch(_) {
panel.innerHTML = '<div style="font-size:11px;color:#555;padding:8px 0">No turntables yet.</div>';
return;
@@ -11689,6 +11817,8 @@
if (img.clip_description)clipDescriptions[img.filename]= img.clip_description;
if (img.pose) filePoses[img.filename] = img.pose;
if (img.prompt) filePrompts[img.filename] = img.prompt;
if (img.description) fileDescriptions[img.filename] = img.description;
if (img.filepath) fileFilepaths[img.filename] = img.filepath;
if (img.group_name && img.group_id) groupNames[img.group_id] = img.group_name;
fileSortOrders[img.filename] = img.sort_order ?? null;
fileHidden[img.filename] = !!img.hidden;
@@ -11732,7 +11862,8 @@
if (bypassStatic) throw new Error("Bypass static requested");
let jsonUrl = `${API}/output/_data/images.json?t=${Date.now()}`;
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
jsonUrl = `${API}/output/_data/group_${LOAD_ONLY_GROUP_ID}.json?t=${Date.now()}`;
const sanitizedGid = LOAD_ONLY_GROUP_ID.replace(/[^a-zA-Z0-9_\-]/g, '_');
jsonUrl = `${API}/output/_data/group_${sanitizedGid}.json?t=${Date.now()}`;
}
const r = await fetch(jsonUrl, { signal: AbortSignal.timeout(4000) });
if (r.ok) {
@@ -12319,6 +12450,19 @@
modelDot.parentElement.title = 'Model-Engine: Status unknown (API server is down)';
}
}
// Apply visibility adjustments based on backend state
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
// Specific group page: hide/show hamburger and studio angle bar toolbar
const sidebarToggle = document.getElementById('sidebarInnerToggle');
if (sidebarToggle) sidebarToggle.style.display = _backendOnline ? '' : 'none';
const angleBar = document.getElementById('studioAngleBar');
if (angleBar) angleBar.style.display = _backendOnline ? '' : 'none';
} else {
// Landing page: hide/show upload field
const uploadZone = document.querySelector('.upload-zone');
if (uploadZone) uploadZone.style.display = _backendOnline ? '' : 'none';
}
}
// Start polling services status every 3 seconds
@@ -12467,7 +12611,12 @@
toggleFile(gid);
}
} else {
openLightbox(gid);
if (typeof LOAD_ONLY_GROUP_ID !== 'undefined' && LOAD_ONLY_GROUP_ID) {
openLightbox(gid);
} else {
const sanitizedGid = gid.replace(/[^a-zA-Z0-9_\-]/g, '_');
window.location.href = `shoot_${sanitizedGid}.html`;
}
}
}
@@ -12504,6 +12653,11 @@
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
const showMR = (n >= 2 && n <= 3);
document.getElementById('multiRefBtn').style.display = showMR ? '' : 'none';
const openStudioBtn = document.getElementById('openStudioBtn');
if (openStudioBtn) {
openStudioBtn.style.display = (n >= 1) ? '' : 'none';
openStudioBtn.textContent = `Open Studio (${n})`;
}
const legend = document.getElementById('multiRefLegend');
if (legend) {
if (showMR) {
@@ -12521,6 +12675,23 @@
}
}
function openStudioForSelected() {
if (selectedFiles.size === 0) {
showToast("Please select at least one group first", "info");
return;
}
window._selectedOrbitGroupIds = Array.from(selectedFiles);
const firstGid = window._selectedOrbitGroupIds[0];
// Turn off selection mode so the studio behaves normally
toggleSelectMode();
openStudio(firstGid, 0);
// Switch to the orbit tab automatically
switchSidebarTab('orbit');
}
function selectAll() {
currentGroups.forEach(g => selectedFiles.add(g.gid));
updateCardSelection();
@@ -12827,6 +12998,12 @@
const pBtn = document.getElementById('privacyBtn');
if (privacyMode && pBtn) {
pBtn.textContent = '🔒';
}
const sBtn = document.getElementById('studioPrivacyBtn');
if (privacyMode && sBtn) {
sBtn.textContent = '🔒';
}
if (privacyMode) {
document.title = PRIV_TITLE;
showPrivacyOverlay();
}
@@ -12892,7 +13069,7 @@
async function suggestUpdatedDescription() {
const fname = lbNames[lbIdx];
if (!fname) return;
showToast('AI is analyzing the image and suggesting a description...', 'info');
showToast('AI is analyzing the image and suggesting description/prompt...', 'info');
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/reverse-engineer`, {
method: 'POST'
@@ -12900,23 +13077,31 @@
if (r.ok) {
const data = await r.json();
const newPrompt = data.prompt;
if (confirm(`Suggested Reconstructed Prompt:\n\n"${newPrompt}"\n\nWould you like to set this as the image generation prompt?`)) {
const updateResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-prompt`, {
const newDesc = data.description;
if (confirm(`Suggested Pose-Directed Prompt:\n"${newPrompt}"\n\nSuggested Character & Scenery Description:\n"${newDesc}"\n\nWould you like to set both?`)) {
const pResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: newPrompt })
});
if (updateResp.ok) {
const dResp = await fetch(`${API}/images/${encodeURIComponent(fname)}/update-description`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ description: newDesc })
});
if (pResp.ok && dResp.ok) {
filePrompts[fname] = newPrompt;
showToast('Successfully updated image prompt!', 'success');
fileDescriptions[fname] = newDesc;
showToast('Successfully updated both prompt and description!', 'success');
updateStudio();
refreshNow();
} else {
showToast('Failed to update image prompt', 'error');
showToast('Failed to update prompt or description', 'error');
}
}
} else {
showToast('Failed to generate suggest description: ' + await r.text(), 'error');
showToast('Failed to generate suggestions: ' + await r.text(), 'error');
}
} catch (err) {
console.error(err);
@@ -12924,6 +13109,113 @@
}
}
function editMetadataField(field) {
const fname = lbNames[lbIdx];
if (!fname) return;
const isPrompt = field === 'prompt';
const contentId = isPrompt ? 'lbGenPrompt' : 'lbDescription';
const currentVal = isPrompt ? (filePrompts[fname] || '') : (fileDescriptions[fname] || '');
const el = document.getElementById(contentId);
if (!el || el.querySelector('textarea')) return;
el.onclick = null;
el.innerHTML = `
<textarea id="editFieldArea" style="width:100%;min-height:80px;background:#0d0d0d;border:1px solid #333;color:#ccc;font-size:11px;font-family:monospace;padding:6px;border-radius:4px;outline:none;resize:vertical;line-height:1.4;margin-bottom:4px">${currentVal}</textarea>
<div style="display:flex;gap:6px;justify-content:flex-end">
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="cancelEditField(event, '${field}')">Cancel</button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px" onclick="refineField(event, '${field}')">✨ Refine</button>
<button class="sb-btn" style="padding:2px 8px;font-size:10px;background:#10b981;color:#fff" onclick="saveEditField(event, '${field}')">Save</button>
</div>
`;
const ta = document.getElementById('editFieldArea');
if (ta) {
ta.focus();
ta.setSelectionRange(ta.value.length, ta.value.length);
}
}
function cancelEditField(event, field) {
if (event) event.stopPropagation();
const isPrompt = field === 'prompt';
const contentId = isPrompt ? 'lbGenPrompt' : 'lbDescription';
const el = document.getElementById(contentId);
if (el) el.innerHTML = '';
updateStudio();
}
async function saveEditField(event, field) {
if (event) event.stopPropagation();
const fname = lbNames[lbIdx];
if (!fname) return;
const ta = document.getElementById('editFieldArea');
if (!ta) return;
const newVal = ta.value.trim();
const isPrompt = field === 'prompt';
const endpoint = isPrompt ? 'update-prompt' : 'update-description';
const bodyKey = isPrompt ? 'prompt' : 'description';
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ [bodyKey]: newVal })
});
if (r.ok) {
if (isPrompt) {
filePrompts[fname] = newVal;
showToast('Pose prompt updated successfully', 'success');
} else {
fileDescriptions[fname] = newVal;
showToast('Character & scenery description updated successfully', 'success');
}
updateStudio();
refreshNow();
} else {
showToast('Failed to save update', 'error');
}
} catch (err) {
console.error(err);
showToast('Error saving update: ' + err, 'error');
}
}
async function refineField(event, field) {
if (event) event.stopPropagation();
const fname = lbNames[lbIdx];
if (!fname) return;
const ta = document.getElementById('editFieldArea');
if (!ta) return;
const currentVal = ta.value.trim();
const instruction = prompt(`Enter instructions to refine this ${field}:`, "Make it more detailed...");
if (!instruction) return;
showToast('AI is refining your text...', 'info');
try {
const r = await fetch(`${API}/refine-prompt`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: currentVal, user_instruction: instruction })
});
if (r.ok) {
const data = await r.json();
ta.value = data.refined;
showToast('Refined successfully! Click Save to apply.', 'success');
} else {
showToast('Failed to refine field: ' + await r.text(), 'error');
}
} catch (err) {
console.error(err);
showToast('Error refining: ' + err, 'error');
}
}
function updatePoseRatioSuggestion() {
const st = window._poseState;
const container = document.getElementById('poseRatioSuggestion');
@@ -13167,6 +13459,26 @@
window.addEventListener('blur', () => { if (privacyMode) showPrivacyOverlay(); });
window.addEventListener('pagehide', () => { if (privacyMode) showPrivacyOverlay(); });
window.addEventListener('storage', (e) => {
if (e.key === 'privacyMode') {
const newPrivacy = e.newValue !== 'false';
if (newPrivacy !== privacyMode) {
privacyMode = newPrivacy;
const btn = document.getElementById('privacyBtn');
if (btn) btn.textContent = privacyMode ? '🔒' : '🔓';
const sBtn = document.getElementById('studioPrivacyBtn');
if (sBtn) sBtn.textContent = privacyMode ? '🔒' : '🔓';
if (privacyMode) {
document.title = PRIV_TITLE;
showPrivacyOverlay();
} else {
document.title = REAL_TITLE;
document.getElementById('privacyOverlay').style.display = 'none';
}
}
}
});
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
if (privacyMode) showPrivacyOverlay();
@@ -13180,9 +13492,12 @@
privacyMode = !privacyMode;
localStorage.setItem('privacyMode', privacyMode);
const btn = document.getElementById('privacyBtn');
btn.textContent = privacyMode ? '🔒' : '🔓';
if (btn) btn.textContent = privacyMode ? '🔒' : '🔓';
const sBtn = document.getElementById('studioPrivacyBtn');
if (sBtn) sBtn.textContent = privacyMode ? '🔒' : '🔓';
if (privacyMode) {
document.title = PRIV_TITLE; // keep the taskbar label discreet even while active
showPrivacyOverlay();
} else {
document.title = REAL_TITLE;
document.getElementById('privacyOverlay').style.display = 'none';
@@ -13752,6 +14067,7 @@
const nSafe = a.name.replace(/'/g, "\\'");
return `<button onclick="submitSingleAngle('${pSafe}', '${nSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
}).join('') + `
<div style="width: 1px; height: 18px; background: rgba(255,255,255,0.15); margin: 0 6px; flex-shrink: 0;"></div>
<button onclick="startManualPad()" title="Pad canvas (Expand image)" style="color:#f59e0b">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="margin:auto;display:block">
<path d="M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"/>