This commit is contained in:
mike
2026-06-28 03:33:02 +02:00
parent 6ad11fc6c0
commit d522b2a267
5 changed files with 914 additions and 28 deletions

View File

@@ -726,6 +726,29 @@
.lb-var-thumb.active { opacity: 1; outline: 2px solid #60a5fa; border-radius: 4px; }
.lb-var-thumb.ref-selected { opacity: 1; outline: 2px solid #f97316; border-radius: 4px; }
.lb-var-thumb.active.ref-selected { outline: 2px solid #f97316; }
.lb-var-thumb.selected {
opacity: 1 !important;
outline: 2px solid #3b82f6 !important;
box-shadow: 0 0 8px rgba(59,130,246,0.6);
border-radius: 4px;
}
.lb-var-thumb-check {
position: absolute;
top: 2px;
right: 2px;
background: #3b82f6;
color: white;
border-radius: 50%;
width: 14px;
height: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 9px;
font-weight: bold;
z-index: 5;
pointer-events: none;
}
.lb-var-ref-badge { position: absolute; top: 2px; left: 2px; background: #f97316; color: #fff; font-size: 9px; font-weight: bold; padding: 1px 4px; border-radius: 3px; line-height: 1.4; pointer-events: none; z-index: 2; }
.lb-var-thumb img {
width: 52px; height: 52px;
@@ -1364,6 +1387,40 @@
align-items: center;
}
.fs-tab {
background: #222;
border: 1px solid #333;
color: #888;
font-size: 11px;
padding: 3px 10px;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
}
.fs-tab.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
font-weight: bold;
}
.fs-tab:hover:not(.active) {
background: #2d2d2d;
border-color: #444;
color: #fff;
}
.fs-action-btn {
background: #1e3a8a;
border: 1px solid #3b82f6;
color: #fff;
font-size: 10px;
padding: 2px 6px;
border-radius: 4px;
cursor: pointer;
}
.fs-action-btn:hover {
background: #3b82f6;
}
/* ===================== SIDEBAR ===================== */
.studio-sidebar {
flex-shrink: 0; width: 320px; min-width: 320px;
@@ -1989,6 +2046,25 @@
</div>
<button class="studio-nav-btn" id="lbNext" onclick="lbNav(1)"></button>
</div>
<!-- Film strip selector tabs -->
<div class="filmstrip-selector-tabs" style="display:flex;gap:4px;padding:4px 8px;background:#151515;border-top:1px solid #1a1a1a;align-items:center;justify-content:space-between;flex-shrink:0">
<div style="display:flex;gap:4px">
<button onclick="switchFilmstripTab('group')" class="fs-tab active" id="fsTabGroup" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'VISIBLE')">Group Variants</button>
<button onclick="switchFilmstripTab('hidden')" class="fs-tab" id="fsTabHidden" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'HIDDEN')">Hidden</button>
<button onclick="switchFilmstripTab('source')" class="fs-tab" id="fsTabSource" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'SOURCE')">Source</button>
<button onclick="switchFilmstripTab('archived')" class="fs-tab" id="fsTabArchived" ondragover="event.preventDefault()" ondrop="filmstripTabDrop(event, 'ARCHIVED')">Archived</button>
</div>
<div style="display:flex;gap:6px;align-items:center" id="multiSelectControls">
<button onclick="toggleMultiSelectMode()" id="multiSelectToggleBtn" style="background:#222;border:1px solid #333;color:#888;font-size:11px;padding:2px 8px;border-radius:4px;cursor:pointer">Multi-select</button>
<div id="multiSelectActions" style="display:none;align-items:center;gap:4px">
<span id="multiSelectCount" style="font-size:11px;color:#aaa">0 selected</span>
<button onclick="bulkMoveSelected('VISIBLE')" class="fs-action-btn">Move to Group</button>
<button onclick="bulkMoveSelected('HIDDEN')" class="fs-action-btn">Move to Hidden</button>
<button onclick="bulkMoveSelected('SOURCE')" class="fs-action-btn">Move to Source</button>
<button onclick="bulkMoveSelected('ARCHIVED')" class="fs-action-btn">Move to Archived</button>
</div>
</div>
</div>
<!-- Film strip / variant thumbnails -->
<div class="studio-filmstrip" id="lbVariantStrip"></div>
<!-- Slideshow controls -->
@@ -2042,7 +2118,9 @@
<button class="sb-btn" id="lbMoveUp" style="display:none" onclick="lbMoveInGroup(-1)" title="Move earlier in group">↑ Earlier</button>
<button class="sb-btn" id="lbMoveDown" style="display:none" onclick="lbMoveInGroup(1)" title="Move later in group">↓ Later</button>
<button class="sb-btn" id="lbPreferredBtn" style="display:none;color:#f59e0b" onclick="lbSetPreferred()" title="Set as preferred reference">★ Preferred</button>
<button class="sb-btn" id="lbSourceBtn" onclick="toggleSourceKeybind()" title="Toggle mark as SOURCE (Shortcut: S)">★ Source</button>
<button class="sb-btn" id="lbEyeBtn" onclick="lbToggleHidden()" title="Hide/show in cycling">👁 Visibility</button>
<button class="sb-btn" id="lbEstimate21Btn" onclick="estimate21PlusKeybind()" title="Detect adult 21+ tags (Shortcut: E)">🔞 21+</button>
</div>
<div class="sb-sep"></div>
<div class="sb-label">Actions</div>
@@ -2227,6 +2305,24 @@
// --- HYDRATION_START ---
const PRELOADED_IMAGES = [
"20260618_053802_image.png_face.png",
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.nobg.png",
"20260628_032232_dup_20260618_053932_7_20260618_053802_image.nobg.nobg.png",
"pass-1.png_face.png",
"20260628_031641_dup_20260628_031119_pad_pass-1.nobg.png",
"20260628_031119_pad_pass-1.nobg.png",
"20260628_031316_pad_pass-1.png",
"20260628_031119_pad_pass-1.png",
"pa01.png_face.png",
"20260628_030838_pa01.png",
"20260628_030808_pa01.png",
"20260628_030624_pa01.png",
"20260628_030532_pa01.png",
"20260628_030241_pa01.png",
"20260628_030115_pa01.png",
"kk563t.png_face.png",
"20260628_025504_pad_pass-1.png",
"up_e25ad102_face.png",
"20260628_020853_pad_20260628_015651_img_12.nobg.png",
"20260628_020642_pad_20260628_015651_img_12.nobg.png",
"20260628_020610_pad_20260628_015651_img_12.nobg.png",
@@ -4605,7 +4701,6 @@
"_turntable/b1.png/views/view_021_315deg.png",
"_turntable/b1.png/views/view_020_300deg.png",
"_turntable/b1.png/views/view_019_285deg.png",
"_turntable/up_e25ad102/turntable.mp4",
"_turntable/up_e25ad102/views/view_011_330deg.png",
"_turntable/up_e25ad102/views/view_010_300deg.png",
"_turntable/up_e25ad102/views/view_009_270deg.png",
@@ -4640,11 +4735,10 @@
"20260626_053719_pad_20260619_224737_img_14.nobg.nobg.png",
"20260626_053754_pad_20260619_224737_img_14.png",
"20260626_053719_pad_20260619_224737_img_14.png",
"20260618_053802_image.png_face.png",
"20260626_043734_7_20260618_053802_image.nobg.nobg.png",
"20260626_043915_crop_20260626_043734_7_20260618_053802_image.nobg.nobg.png",
"20260626_043734_7_20260618_053802_image.nobg.png",
"20260626_043529_pad_20260618_053932_7_20260618_053802_image.nobg.png",
"20260628_032038_dup_20260618_053932_7_20260618_053802_image.nobg.png",
"20260618_053932_7_20260618_053802_image.nobg.png",
"20260626_041550_sc_pad_20260626_034830_jb.nobg.png",
"20260626_041539_sc_pad_20260626_034830_jb.nobg.png",
@@ -4776,8 +4870,8 @@
"20260625_025228_dup_20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_025156_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_025115_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_025335_dup_20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_024438_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_023536_dup_20260624_165109_0_20260622_091731_image.nobg.png",
"20260624_165109_0_20260622_091731_image.nobg.png",
"20260625_023536_dup_20260624_165109_0_20260622_091731_image.png",
@@ -4791,10 +4885,9 @@
"20260625_021910_image.png",
"20260625_021702_sc_image.png",
"20260625_021615_sc_image.png",
"20260625_020212_dup_20260625_015711_sc_image.png",
"20260625_015711_sc_image.png",
"20260625_020212_dup_20260625_015711_sc_image.png",
"20260625_015711_sc_image.nobg.png",
"up_e25ad102_face.png",
"20260625_015900_sc_image.png",
"20260625_015413_sc_image.png",
"20260625_015213_image.png",
@@ -4980,8 +5073,8 @@
"20260624_183010_0_20260624_182958_image.png",
"20260624_182958_image.png",
"20260624_174900_image.png",
"20260624_174943_dup_20260624_173115_image.png",
"20260624_175711_dup_20260624_174943_dup_20260624_173115_image.png",
"20260624_174943_dup_20260624_173115_image.png",
"20260624_173115_image.png",
"20260624_174757_image.png",
"20260624_174442_image.png",
@@ -5007,8 +5100,8 @@
"20260624_164240_crop_20260624_162342_1_20260622_093058_image.png",
"20260624_164218_crop_20260624_162601_mr_0_20260622_093058_image.nobg.png",
"20260624_164059_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_164116_dup_20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_163951_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_163634_dup_20260624_163352_dup_20260624_162601_mr_0_20260622_093058_image.png",
"20260624_162510_image.png",
"20260624_162408_image.png",
@@ -6459,7 +6552,6 @@
"20260618_171628_image.png",
"20260618_171624_0_20260618_171448_image.png",
"20260618_171448_image.png",
"20260618_134543_8_20260618_134348_image.png",
"20260618_134532_7_20260618_134348_image.png",
"20260618_134520_6_20260618_134348_image.png",
"20260618_134508_5_20260618_134348_image.png",
@@ -6560,7 +6652,6 @@
"20260618_054252_1_20260618_054230_image.png",
"20260618_054241_0_20260618_054230_image.png",
"20260618_054230_image.png",
"20260618_053932_7_20260618_053802_image.png",
"20260618_053920_6_20260618_053802_image.png",
"20260618_053909_5_20260618_053802_image.png",
"20260618_053857_4_20260618_053802_image.png",
@@ -6710,7 +6801,10 @@
// Supported extensions
const EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg', '.mp4', '.mov', '.webm'];
const VIDEO_EXTENSIONS = new Set(['.mp4', '.mov', '.webm', '.avi', '.mkv']);
function isVideo(name) { return VIDEO_EXTENSIONS.has((name.split('.').pop() ? '.'+name.split('.').pop().toLowerCase() : '')); }
function isVideo(name) {
if (!name || typeof name !== 'string') return false;
return VIDEO_EXTENSIONS.has((name.split('.').pop() ? '.'+name.split('.').pop().toLowerCase() : ''));
}
// Videos can't render as an <img> thumbnail over file://, so the backend
// writes a sibling "<stem>.jpg" poster. Map a video URL → its poster URL
// (preserving the ?t= cache-buster). Non-video URLs pass through unchanged.
@@ -6720,6 +6814,16 @@
const path = q === -1 ? url : url.slice(0, q);
const qs = q === -1 ? '' : url.slice(q);
if (!/\.(mp4|mov|webm|avi|mkv)$/i.test(path)) return url;
// For turntable/orbit videos, use the first view frame as the poster/thumbnail instead of a non-existent .jpg
if (path.includes('_turntable/')) {
const lastSlash = path.lastIndexOf('/');
if (lastSlash !== -1) {
const dir = path.substring(0, lastSlash);
return dir + '/views/view_000_000deg.png' + qs;
}
}
return path.replace(/\.[^.]+$/, '.jpg') + qs;
}
@@ -6747,6 +6851,8 @@
let filePrompts = {}; // filename → generation prompt
let fileHidden = {}; // filename → boolean (hidden from cycling)
let fileArchived = {}; // filename → boolean
let fileTags = {}; // filename → array of strings
let fileIsSource = {}; // filename → boolean
let _archiveViewActive = false; // when true, show archived items only
let fileHasBg = {}; // filename → boolean (has background, default true)
let fileSourceRefs = {}; // filename → array of source filenames
@@ -6873,6 +6979,242 @@
// --- studio view (replaces lightbox) ---
let lbUrls = [], lbNames = [], lbIdx = 0;
let _activeFilmstripTab = 'group'; // 'group', 'hidden', 'source', 'archived'
let _multiSelectModeActive = false;
const _selectedFilenames = new Set();
function switchFilmstripTab(tab) {
_activeFilmstripTab = tab;
// Highlight tab
['Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === tab);
});
// Reset selection when switching tabs
_selectedFilenames.clear();
updateMultiSelectUI();
const currentImg = lbNames[lbIdx];
if (tab === 'group') {
const data = groupData.get(lbCurrentGid);
if (data) {
lbUrls = data.urls;
lbNames = data.names;
}
} else {
const matched = [];
knownFiles.forEach(fname => {
const isArchived = !!fileArchived[fname];
const isHidden = !!fileHidden[fname];
const isSource = !!fileIsSource[fname];
if (tab === 'hidden' && isHidden && !isArchived) {
matched.push(fname);
} else if (tab === 'source' && isSource && !isArchived) {
matched.push(fname);
} else if (tab === 'archived' && isArchived) {
matched.push(fname);
}
});
// Sort matching files alphabetically (descending time-like sort)
matched.sort((a, b) => b.localeCompare(a));
lbNames = matched;
lbUrls = matched.map(n => IMAGE_FOLDER + n);
}
let newIdx = lbNames.indexOf(currentImg);
if (newIdx === -1) newIdx = 0;
lbIdx = newIdx;
updateStudio();
}
function toggleMultiSelectMode() {
_multiSelectModeActive = !_multiSelectModeActive;
const btn = document.getElementById('multiSelectToggleBtn');
if (btn) {
btn.textContent = _multiSelectModeActive ? 'Cancel Multi' : 'Multi-select';
btn.style.background = _multiSelectModeActive ? '#1e3a8a' : '#222';
}
_selectedFilenames.clear();
updateMultiSelectUI();
updateStudio(); // Re-render thumbnails to show checkmarks
}
function updateMultiSelectUI() {
const actionsEl = document.getElementById('multiSelectActions');
const countEl = document.getElementById('multiSelectCount');
if (actionsEl && countEl) {
if (_multiSelectModeActive && _selectedFilenames.size > 0) {
actionsEl.style.display = 'flex';
countEl.textContent = `${_selectedFilenames.size} selected`;
} else {
actionsEl.style.display = 'none';
}
}
}
async function bulkMoveSelected(targetStrip) {
if (_selectedFilenames.size === 0) return;
const list = Array.from(_selectedFilenames);
try {
const r = await fetch(`${API}/images/bulk-move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames: list, target_strip: targetStrip })
});
if (r.ok) {
showToast(`Moved ${list.length} images to ${targetStrip}`);
// Update local cache immediately
list.forEach(fname => {
if (targetStrip === 'VISIBLE') {
fileArchived[fname] = false;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'HIDDEN');
if (!fileTags[fname].includes('VISIBLE')) fileTags[fname].push('VISIBLE');
}
} else if (targetStrip === 'HIDDEN') {
fileArchived[fname] = false;
fileHidden[fname] = true;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'VISIBLE');
if (!fileTags[fname].includes('HIDDEN')) fileTags[fname].push('HIDDEN');
}
} else if (targetStrip === 'ARCHIVED') {
fileArchived[fname] = true;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'HIDDEN' && t !== 'VISIBLE');
if (!fileTags[fname].includes('ARCHIVED')) fileTags[fname].push('ARCHIVED');
}
} else if (targetStrip === 'SOURCE') {
fileIsSource[fname] = true;
if (fileTags[fname] && !fileTags[fname].includes('SOURCE')) {
fileTags[fname].push('SOURCE');
}
}
});
toggleMultiSelectMode();
switchFilmstripTab(_activeFilmstripTab);
refreshNow();
} else {
const err = await r.text();
showToast(`Failed to move: ${err}`, 'error');
}
} catch (e) {
showToast(`Network error: ${e}`, 'error');
}
}
function filmstripThumbDragStart(event, fname) {
event.dataTransfer.setData('text/plain', fname);
event.dataTransfer.effectAllowed = 'move';
}
async function filmstripTabDrop(event, targetStrip) {
event.preventDefault();
const fname = event.dataTransfer.getData('text/plain');
if (!fname) return;
try {
const r = await fetch(`${API}/images/bulk-move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filenames: [fname], target_strip: targetStrip })
});
if (r.ok) {
showToast(`Moved ${fname} to ${targetStrip}`);
if (targetStrip === 'VISIBLE') {
fileArchived[fname] = false;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'HIDDEN');
if (!fileTags[fname].includes('VISIBLE')) fileTags[fname].push('VISIBLE');
}
} else if (targetStrip === 'HIDDEN') {
fileArchived[fname] = false;
fileHidden[fname] = true;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'ARCHIVED' && t !== 'VISIBLE');
if (!fileTags[fname].includes('HIDDEN')) fileTags[fname].push('HIDDEN');
}
} else if (targetStrip === 'ARCHIVED') {
fileArchived[fname] = true;
fileHidden[fname] = false;
if (fileTags[fname]) {
fileTags[fname] = fileTags[fname].filter(t => t !== 'HIDDEN' && t !== 'VISIBLE');
if (!fileTags[fname].includes('ARCHIVED')) fileTags[fname].push('ARCHIVED');
}
} else if (targetStrip === 'SOURCE') {
fileIsSource[fname] = true;
if (fileTags[fname] && !fileTags[fname].includes('SOURCE')) {
fileTags[fname].push('SOURCE');
}
}
switchFilmstripTab(_activeFilmstripTab);
refreshNow();
}
} catch (e) {
showToast(`Drag-drop move failed: ${e}`, 'error');
}
}
async function toggleSourceKeybind() {
const fname = lbNames[lbIdx];
if (!fname) return;
const newSource = !fileIsSource[fname];
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/source`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ source: newSource })
});
if (r.ok) {
fileIsSource[fname] = newSource;
if (newSource) {
if (fileTags[fname] && !fileTags[fname].includes('SOURCE')) fileTags[fname].push('SOURCE');
} else {
if (fileTags[fname]) fileTags[fname] = fileTags[fname].filter(t => t !== 'SOURCE');
}
showToast(newSource ? 'Marked as SOURCE' : 'Unmarked as SOURCE');
updateStudio();
refreshNow();
}
} catch (err) {
showToast(`Failed to toggle source: ${err}`, 'error');
}
}
async function estimate21PlusKeybind() {
const fname = lbNames[lbIdx];
if (!fname) return;
showToast('Estimating 21+ tag...', 'info');
try {
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/estimate-21plus`, {
method: 'POST'
});
if (r.ok) {
const data = await r.json();
const is21 = !!data['21+'];
fileTags[fname] = data.tags;
showToast(is21 ? 'Tagged as 21+' : 'Not tagged as 21+');
updateStudio();
refreshNow();
}
} catch (err) {
showToast(`Failed to estimate: ${err}`, 'error');
}
}
let _activeSidebarTab = localStorage.getItem('studioSidebarTab') || 'info';
// Maps fname → nobg sidecar URL; viewer shows sidecar while this entry exists
const _nobgSidecars = new Map();
@@ -6896,6 +7238,16 @@
lbCurrentGid = gid;
_lastSelectedGid = gid;
localStorage.setItem('lastSelectedGid', gid);
_activeFilmstripTab = 'group';
['Group', 'Hidden', 'Source', 'Archived'].forEach(t => {
const el = document.getElementById(`fsTab${t}`);
if (el) el.classList.toggle('active', t.toLowerCase() === 'group');
});
_multiSelectModeActive = false;
_selectedFilenames.clear();
updateMultiSelectUI();
lbUrls = data.urls;
lbNames = data.names;
lbIdx = startIdx !== undefined ? startIdx : (cycleIdx.get(gid) || 0);
@@ -7297,6 +7649,18 @@
}
function sbFilmstripClick(event, i) {
const fname = lbNames[i];
if (_multiSelectModeActive) {
event.preventDefault();
if (_selectedFilenames.has(fname)) {
_selectedFilenames.delete(fname);
} else {
_selectedFilenames.add(fname);
}
updateMultiSelectUI();
updateStudio();
return;
}
if (event.shiftKey) {
event.preventDefault();
// Scenery tab uses positional slots (fills right→left); Generate uses _sbRefIndices.
@@ -7327,6 +7691,74 @@
const lbImgEl = document.getElementById('lbImg');
const lbVideoEl = document.getElementById('lbVideo');
if (!fname) {
lbImgEl.style.display = 'none';
lbImgEl.src = '';
lbVideoEl.style.display = 'none';
lbVideoEl.src = '';
const dimEl = document.getElementById('lbImgDims');
if (dimEl) dimEl.textContent = '';
document.getElementById('lbCounter').textContent = '';
const prevBtn = document.getElementById('lbPrev');
const nextBtn = document.getElementById('lbNext');
if (prevBtn) prevBtn.style.visibility = 'hidden';
if (nextBtn) nextBtn.style.visibility = 'hidden';
const extractBtn = document.getElementById('lbExtract');
if (extractBtn) extractBtn.style.display = 'none';
const moveUp = document.getElementById('lbMoveUp');
const moveDown = document.getElementById('lbMoveDown');
if (moveUp) moveUp.style.display = 'none';
if (moveDown) moveDown.style.display = 'none';
const prefBtn = document.getElementById('lbPreferredBtn');
if (prefBtn) prefBtn.style.display = 'none';
const groupNameInput = document.getElementById('lbGroupNameInput');
if (groupNameInput) groupNameInput.value = '';
const nameInput = document.getElementById('lbNameInput');
if (nameInput) { nameInput.value = ''; nameInput.placeholder = ''; }
const clipEl = document.getElementById('lbClipDesc');
if (clipEl) clipEl.textContent = 'No images in this tab.';
const genPromptWrap = document.getElementById('lbGenPromptWrap');
if (genPromptWrap) genPromptWrap.style.display = 'none';
const poseTag = document.getElementById('lbPoseTag');
if (poseTag) poseTag.style.display = 'none';
const noBgBtn = document.getElementById('lbNoBgBtn');
const undressBtn = document.getElementById('lbUndressBtn');
const faceswapBtn = document.getElementById('lbFaceswapBtn');
if (noBgBtn) noBgBtn.style.display = 'none';
if (undressBtn) undressBtn.style.display = 'none';
if (faceswapBtn)faceswapBtn.style.display = 'none';
const cropBtn = document.getElementById('lbCropBtn');
if (cropBtn) cropBtn.style.display = 'none';
const invAlphaBtn = document.getElementById('lbInvertAlphaBtn');
if (invAlphaBtn) invAlphaBtn.style.display = 'none';
const poseBtn = document.getElementById('lbPoseBtn');
if (poseBtn) poseBtn.style.display = 'none';
const refsDiv = document.getElementById('lbSourceRefs');
if (refsDiv) refsDiv.style.display = 'none';
const strip = document.getElementById('lbVariantStrip');
if (strip) strip.innerHTML = '<div style="color:#666;font-size:11px;padding:12px;width:100%;text-align:center">No images in this strip</div>';
updateSbAngleBar();
const faceBook = document.getElementById('lbFaceBook');
if (faceBook) faceBook.style.display = 'none';
updatePadPreview();
toggleCheckerboard(false);
return;
}
lbImgEl.onload = () => {
const dimEl = document.getElementById('lbImgDims');
if (dimEl) dimEl.textContent = `${lbImgEl.naturalWidth} x ${lbImgEl.naturalHeight}`;
@@ -7402,6 +7834,21 @@
const eyeBtn = document.getElementById('lbEyeBtn');
if (eyeBtn) { eyeBtn.textContent = hidden ? '🚫 Hidden' : '👁 Visibility'; eyeBtn.title = hidden ? 'Show in cycling' : 'Hide from cycling'; }
const sourceBtn = document.getElementById('lbSourceBtn');
if (sourceBtn) {
const isSrc = !!fileIsSource[fname];
sourceBtn.style.color = isSrc ? '#f59e0b' : '';
sourceBtn.style.borderColor = isSrc ? '#f59e0b' : '';
sourceBtn.textContent = isSrc ? '★ Source (Yes)' : '★ Source';
}
const estimateBtn = document.getElementById('lbEstimate21Btn');
if (estimateBtn) {
const is21 = fileTags[fname] && fileTags[fname].includes('21+');
estimateBtn.style.color = is21 ? '#ef4444' : '';
estimateBtn.style.borderColor = is21 ? '#ef4444' : '';
estimateBtn.textContent = is21 ? '🔞 21+ (Yes)' : '🔞 21+';
}
const noBgBtn = document.getElementById('lbNoBgBtn');
const undressBtn = document.getElementById('lbUndressBtn');
const faceswapBtn = document.getElementById('lbFaceswapBtn');
@@ -7460,17 +7907,23 @@
const n = lbNames[i];
const pose = filePoses[n] || '';
const act = i === lbIdx ? ' active' : '';
const isSel = _selectedFilenames.has(n);
const selCls = isSel ? ' selected' : '';
// Badge: Generate uses _sbRefIndices order; Scenery uses positional slots.
const badge = _filmstripRefBadge(i);
const refCls = badge > 0 ? ' ref-selected' : '';
const thumbSrc = isVideo(n) ? posterFor(url) : url;
// On the Scenery tab the thumbs are draggable onto the slots above.
const dragAttr = sceneryActive ? ' draggable="true" ondragstart="sceneFilmDragStart(event,' + i + ')"' : '';
return '<div class="lb-var-thumb' + act + refCls + '" onclick="sbFilmstripClick(event,' + i + ')"' + dragAttr + ' title="Click to view · Shift+Click to add as ref">'
// On the Scenery tab the thumbs are draggable onto the slots above. Otherwise draggable for strip drop targets
const dragAttr = sceneryActive
? ' draggable="true" ondragstart="sceneFilmDragStart(event,' + i + ')"'
: ' draggable="true" ondragstart="filmstripThumbDragStart(event,\'' + n.replace(/'/g, "\\'") + '\')"';
const titleStr = _multiSelectModeActive ? 'Click to select' : 'Click to view · Shift+Click to add as ref';
return '<div class="lb-var-thumb' + act + refCls + selCls + '" onclick="sbFilmstripClick(event,' + i + ')"' + dragAttr + ' title="' + titleStr + '">'
+ '<img src="' + thumbSrc + '" loading="lazy" onerror="this.style.opacity=\'0.3\'">'
+ (isVideo(n) ? '<div class="lb-var-play">▶</div>' : '')
+ (pose ? '<div class="lb-var-pose">' + escHtml(pose) + '</div>' : '')
+ (badge > 0 ? '<div class="lb-var-ref-badge">#' + badge + '</div>' : '')
+ (isSel ? '<div class="lb-var-thumb-check">✓</div>' : '')
+ '</div>';
}).join('');
const active = strip.querySelector('.lb-var-thumb.active');
@@ -9391,6 +9844,8 @@
fileSortOrders[img.filename] = img.sort_order ?? null;
fileHidden[img.filename] = !!img.hidden;
fileArchived[img.filename] = !!img.archived;
fileTags[img.filename] = img.tags || [];
fileIsSource[img.filename] = !!img.is_source;
fileHasBg[img.filename] = img.has_background !== false;
fileHasClothing[img.filename] = img.has_clothing ?? null;
fileContentType[img.filename] = img.content_type || 'image';
@@ -9419,9 +9874,9 @@
if (r.ok) {
const data = await r.json();
let imgs = data.images || [];
_hydrateImageMeta(imgs); // Hydrate everything first!
imgs = _archiveViewActive ? imgs.filter(i => i.archived) : imgs.filter(i => !i.archived);
files = imgs.map(img => img.filename);
_hydrateImageMeta(imgs);
_staticOk = true;
}
} catch (e) { /* fall through */ }
@@ -9429,12 +9884,13 @@
// Fallback: live API (handles first startup before static file is written)
if (!files) {
try {
const url = _archiveViewActive ? `${API}/images?archived=true` : `${API}/images`;
const url = `${API}/images?archived=true`;
const r = await fetch(url, { signal: AbortSignal.timeout(12000) });
if (r.ok) {
const data = await r.json();
files = data.images.map(img => img.filename);
_hydrateImageMeta(data.images);
_hydrateImageMeta(data.images); // Hydrate everything first!
let imgs = data.images.filter(i => _archiveViewActive ? i.archived : !i.archived);
files = imgs.map(img => img.filename);
}
} catch (e) { /* API not reachable, fall through */ }
}
@@ -10200,6 +10656,14 @@
lbSetPreferred();
e.preventDefault();
}
if ((e.key === 's' || e.key === 'S') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
toggleSourceKeybind();
e.preventDefault();
}
if ((e.key === 'e' || e.key === 'E') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
estimate21PlusKeybind();
e.preventDefault();
}
if (e.key === 'Delete') {
lbArchive();
e.preventDefault();