aa
This commit is contained in:
@@ -649,6 +649,92 @@
|
||||
|
||||
#uploadInput { display: none; }
|
||||
|
||||
/* --- lightbox variant strip --- */
|
||||
#lbVariantStrip {
|
||||
display: none;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding: 6px 2px 2px;
|
||||
width: 100%;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #444 transparent;
|
||||
}
|
||||
.lb-var-thumb {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s;
|
||||
max-width: 60px;
|
||||
}
|
||||
.lb-var-thumb:hover { opacity: 0.85; }
|
||||
.lb-var-thumb.active { opacity: 1; outline: 2px solid #60a5fa; border-radius: 4px; }
|
||||
.lb-var-thumb img {
|
||||
width: 52px; height: 52px;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
}
|
||||
.lb-var-pose {
|
||||
font-size: 7px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 58px;
|
||||
}
|
||||
|
||||
/* --- group card grid overview --- */
|
||||
.card-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 2px;
|
||||
background: #111;
|
||||
}
|
||||
.card-grid-thumb {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
}
|
||||
.card-grid-thumb img {
|
||||
width: 100%; height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.card-grid-thumb .thumb-pose {
|
||||
position: absolute;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
background: rgba(0,0,0,0.72);
|
||||
font-size: 7px;
|
||||
color: #ccc;
|
||||
padding: 1px 3px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
.card-grid-more {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- privacy overlay --- */
|
||||
.privacy-overlay {
|
||||
display: none;
|
||||
@@ -718,6 +804,7 @@
|
||||
<button class="btn primary" onclick="refreshNow()">Refresh Now</button>
|
||||
<button class="btn" id="selectBtn" onclick="toggleSelectMode()">Select</button>
|
||||
<button class="btn" id="privacyBtn" onclick="togglePrivacyMode()" title="Privacy mode — hides content on focus loss (P)">🔓</button>
|
||||
<button class="btn" onclick="cleanupDb()" title="Remove DB records for deleted/missing files" style="font-size:11px;opacity:0.6">Clean DB</button>
|
||||
</div>
|
||||
<div class="status">
|
||||
<span class="status-dot" id="statusDot"></span>
|
||||
@@ -764,11 +851,13 @@
|
||||
<button class="lb-btn" id="lbMoveDown" style="font-size:13px;padding:3px 8px;display:none" onclick="lbMoveInGroup(1)" title="Move later in group">↓</button>
|
||||
<button class="lb-btn" id="lbPreferredBtn" style="font-size:11px;padding:3px 8px;display:none;color:#f59e0b" onclick="lbSetPreferred()" title="Set as preferred reference for this group">★ Preferred</button>
|
||||
<button class="lb-btn" id="lbEyeBtn" style="font-size:15px;padding:3px 8px" onclick="lbToggleHidden()" title="Hide/show this image in group cycling">👁</button>
|
||||
<button class="lb-btn" style="font-size:11px;padding:3px 10px" onclick="tagCurrentImage()">Tag</button>
|
||||
<button class="lb-btn" style="font-size:11px;padding:3px 10px" onclick="tagCurrentImage()" title="Re-run tagger to update tags and clothing detection">Re-tag</button>
|
||||
<button class="lb-btn" id="lbUndressBtn" style="font-size:11px;padding:3px 10px;background:#7c3aed;display:none" onclick="lbUndress()" title="Generate without clothing">Undress</button>
|
||||
<button class="lb-btn" id="lbExtract" style="font-size:11px;padding:3px 10px" onclick="extractCurrentImage()">Extract</button>
|
||||
<button class="lb-btn" id="lbNoBgBtn" style="font-size:11px;padding:3px 10px;background: #334155" onclick="lbRemoveBg()">No BG</button>
|
||||
<button class="lb-btn" style="font-size:11px;padding:3px 10px;background: #991b1b" onclick="lbDelete()">Delete</button>
|
||||
<div id="lbSourceRefs" style="display:none;align-items:center;gap:4px;flex-wrap:wrap;margin-top:4px;width:100%"></div>
|
||||
<div id="lbVariantStrip"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1068,8 +1157,13 @@
|
||||
let fileHidden = {}; // filename → boolean (hidden from cycling)
|
||||
let fileHasBg = {}; // filename → boolean (has background, default true)
|
||||
let fileSourceRefs = {}; // filename → array of source filenames
|
||||
let fileHasClothing = {}; // filename → boolean|null (null = unknown)
|
||||
let privacyMode = localStorage.getItem('privacyMode') === 'true';
|
||||
|
||||
function escHtml(s) {
|
||||
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
|
||||
function getBaseName(name) {
|
||||
return name.replace(/^(\d{8}_\d{6}_)+/, '');
|
||||
}
|
||||
@@ -1155,6 +1249,11 @@
|
||||
updateLightbox();
|
||||
}
|
||||
|
||||
function lbNavTo(idx) {
|
||||
lbIdx = idx;
|
||||
updateLightbox();
|
||||
}
|
||||
|
||||
function updateLightbox() {
|
||||
document.getElementById('lbImg').src = lbUrls[lbIdx];
|
||||
const fname = lbNames[lbIdx];
|
||||
@@ -1181,7 +1280,8 @@
|
||||
document.getElementById('lbEyeBtn').title = hidden ? 'Show in cycling' : 'Hide from cycling';
|
||||
|
||||
// No BG button — hide when image already has no background
|
||||
document.getElementById('lbNoBgBtn').style.display = fileHasBg[fname] !== false ? '' : 'none';
|
||||
document.getElementById('lbNoBgBtn').style.display = fileHasBg[fname] !== false ? '' : 'none';
|
||||
document.getElementById('lbUndressBtn').style.display = fileHasClothing[fname] === true ? '' : 'none';
|
||||
|
||||
// Source refs — show small clickable thumbnails
|
||||
const refs = fileSourceRefs[fname];
|
||||
@@ -1207,6 +1307,26 @@
|
||||
// ★ Preferred — shown when not already the first image
|
||||
const prefBtn = document.getElementById('lbPreferredBtn');
|
||||
prefBtn.style.display = multi && lbIdx > 0 ? '' : 'none';
|
||||
|
||||
// Variant strip — thumbnails + pose labels for every image in the group
|
||||
const strip = document.getElementById('lbVariantStrip');
|
||||
if (multi) {
|
||||
strip.style.display = 'flex';
|
||||
strip.innerHTML = lbUrls.map((url, i) => {
|
||||
const n = lbNames[i];
|
||||
const pose = filePoses[n] || '';
|
||||
const act = i === lbIdx ? ' active' : '';
|
||||
return '<div class="lb-var-thumb' + act + '" onclick="lbNavTo(' + i + ')">'
|
||||
+ '<img src="' + url + '" loading="lazy" onerror="this.style.opacity=\'0.3\'">'
|
||||
+ (pose ? '<div class="lb-var-pose">' + escHtml(pose) + '</div>' : '')
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
// Scroll active thumb into view
|
||||
const active = strip.querySelector('.lb-var-thumb.active');
|
||||
if (active) active.scrollIntoView({ block: 'nearest', inline: 'center' });
|
||||
} else {
|
||||
strip.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function updateName() {
|
||||
@@ -1524,7 +1644,7 @@
|
||||
|
||||
// Primary: live list from API (always up to date, works after batch jobs)
|
||||
try {
|
||||
const r = await fetch(`${API}/images`, { signal: AbortSignal.timeout(3000) });
|
||||
const r = await fetch(`${API}/images`, { signal: AbortSignal.timeout(12000) });
|
||||
if (r.ok) {
|
||||
const data = await r.json();
|
||||
files = data.images.map(img => img.filename);
|
||||
@@ -1537,7 +1657,8 @@
|
||||
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;
|
||||
fileHasBg[img.filename] = img.has_background !== false;
|
||||
fileHasBg[img.filename] = img.has_background !== false;
|
||||
fileHasClothing[img.filename] = img.has_clothing ?? null;
|
||||
if (img.source_refs) {
|
||||
try { fileSourceRefs[img.filename] = JSON.parse(img.source_refs); } catch(e) {}
|
||||
}
|
||||
@@ -1570,16 +1691,19 @@
|
||||
}
|
||||
|
||||
if (fileObjects.length === 0) {
|
||||
gallery.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1 / -1;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<path d="M21 15l-5-5L5 21"/>
|
||||
</svg>
|
||||
<h2>No images found</h2>
|
||||
<p>Place images in the output folder and refresh.</p>
|
||||
</div>`;
|
||||
// Only show "no images" if gallery is currently empty (avoid flash on slow API)
|
||||
if (!gallery.querySelector('.image-card')) {
|
||||
gallery.innerHTML = `
|
||||
<div class="empty-state" style="grid-column: 1 / -1;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<path d="M21 15l-5-5L5 21"/>
|
||||
</svg>
|
||||
<h2>No images found</h2>
|
||||
<p>Place images in the output folder and refresh.</p>
|
||||
</div>`;
|
||||
}
|
||||
statusDot.classList.remove('updating');
|
||||
return;
|
||||
}
|
||||
@@ -1615,7 +1739,6 @@
|
||||
const label = groupDisplayName(group);
|
||||
const safeGid = gid.replace(/"/g, '"');
|
||||
const poses = [...new Set(gf.map(f => filePoses[f.cleanName]).filter(Boolean))];
|
||||
// First visible image (fallback to first overall)
|
||||
const firstVisUrl = visibleIdx.length ? urls[visibleIdx[0]] : urls[0];
|
||||
|
||||
return `
|
||||
@@ -1635,22 +1758,20 @@
|
||||
<div class="image-wrapper">
|
||||
<div class="select-indicator"></div>
|
||||
${vCount > 1 ? `<div class="variant-count">${vCount}×${count !== vCount ? `<span style="opacity:.5;font-size:9px"> /${count}</span>` : ''}</div>` : ''}
|
||||
<img src="${firstVisUrl}" alt="${label}" loading="lazy"
|
||||
onerror="this.style.opacity='0.2'">
|
||||
<img src="${firstVisUrl}" alt="${label}" loading="${isRecent ? 'eager' : 'lazy'}" onerror="this.style.opacity='0.2'">
|
||||
${vCount > 1 ? `<div class="variant-dots">${visibleIdx.map((_, i) => `<span class="dot${i === 0 ? ' active' : ''}"></span>`).join('')}</div>` : ''}
|
||||
</div>
|
||||
<div class="image-info">
|
||||
<div class="image-name" title="${label}">${label}</div>
|
||||
<div class="image-meta">
|
||||
<span class="image-time">#${String(currentGroups.length - gIdx).padStart(3, '0')}</span>
|
||||
${poses.length ? `<span class="badge" style="background:#78350f;font-size:9px">${poses[0]}</span>` : ''}
|
||||
${poses.length ? `<span class="badge" style="background:#78350f;font-size:9px">${escHtml(poses[0])}</span>` : ''}
|
||||
${isRecent ? '<span class="badge recent">Latest</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Start cycling for groups with 2+ visible images
|
||||
currentGroups.forEach(g => {
|
||||
const data = groupData.get(g.gid);
|
||||
if (data && data.visibleIdx && data.visibleIdx.length > 1) startGroupCycle(g.gid);
|
||||
@@ -2153,20 +2274,16 @@
|
||||
});
|
||||
});
|
||||
|
||||
// Privacy overlay — show when window loses focus, hide on regain
|
||||
// Privacy overlay — show on any focus loss; requires explicit dismiss (click overlay or P)
|
||||
window.addEventListener('blur', () => {
|
||||
if (privacyMode) document.getElementById('privacyOverlay').style.display = 'flex';
|
||||
});
|
||||
window.addEventListener('focus', () => {
|
||||
document.getElementById('privacyOverlay').style.display = 'none';
|
||||
});
|
||||
|
||||
// Handle visibility change — refresh when tab becomes active; privacy on hide
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
if (privacyMode) document.getElementById('privacyOverlay').style.display = 'flex';
|
||||
} else {
|
||||
document.getElementById('privacyOverlay').style.display = 'none';
|
||||
// Don't auto-hide — user must click overlay or press P to dismiss
|
||||
loadImages();
|
||||
}
|
||||
});
|
||||
@@ -2196,6 +2313,35 @@
|
||||
} catch(e) { console.error(e); }
|
||||
}
|
||||
|
||||
async function cleanupDb() {
|
||||
try {
|
||||
const r = await fetch(`${API}/db/cleanup`, { method: 'POST' });
|
||||
const d = await r.json();
|
||||
showToast(`Cleaned ${d.removed} orphan record${d.removed !== 1 ? 's' : ''}`);
|
||||
if (d.removed > 0) refreshNow();
|
||||
} catch(e) {
|
||||
showToast('Cleanup failed');
|
||||
}
|
||||
}
|
||||
|
||||
async function lbUndress() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
const btn = document.getElementById('lbUndressBtn');
|
||||
btn.textContent = '...';
|
||||
btn.disabled = true;
|
||||
try {
|
||||
const r = await fetch(`${API}/images/${encodeURIComponent(fname)}/undress`, {method:'POST'});
|
||||
if (!r.ok) { showToast('Undress failed', 'error'); return; }
|
||||
showToast('Undress queued', 'success');
|
||||
} catch(e) {
|
||||
showToast('Undress error', 'error');
|
||||
} finally {
|
||||
btn.textContent = 'Undress';
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function lbSetPreferred() {
|
||||
const fname = lbNames[lbIdx];
|
||||
if (!fname) return;
|
||||
|
||||
Reference in New Issue
Block a user