This commit is contained in:
mike
2026-06-27 15:07:38 +02:00
parent 08a8cc9b82
commit 35306327f7
2 changed files with 114 additions and 14 deletions

View File

@@ -126,6 +126,19 @@
opacity: 1;
}
.image-card.highlight-prev {
border: 2px solid #f59e0b !important;
box-shadow: 0 0 15px rgba(245, 158, 11, 0.4);
}
.pad-preview-overlay {
position: absolute;
background: rgba(245,158,11,0.25);
border: 1px dashed #f59e0b;
pointer-events: none;
z-index: 5;
}
.action-btn {
background: rgba(0,0,0,0.6);
backdrop-filter: blur(4px);
@@ -1902,6 +1915,7 @@
<div class="studio-topbar">
<div class="studio-topbar-left">
<button class="btn" onclick="closeStudio()" style="font-size:11px;padding:4px 10px">✕ Gallery</button>
<button class="btn" id="lbBackBtn" onclick="jumpToPrevGroup()" style="font-size:11px;padding:4px 10px;display:none" title="Go back to previously opened group">↺ Back</button>
</div>
<div class="studio-topbar-center">
<input type="text" id="lbGroupNameInput"
@@ -2426,8 +2440,11 @@
let currentFiles = [];
let currentGroups = [];
const groupData = new Map(); // gid → { urls[], names[] }
const fileGroups = {}; // filename → gid (for jumpToImage)
const cycleTimers = new Map(); // gid → intervalId
const cycleIdx = new Map(); // gid → current index
let _lastSelectedGid = localStorage.getItem('lastSelectedGid') || null;
let _prevGid = null; // for "Back" button in studio
let imageNames = {}; // filename → human name (from /names)
let clipDescriptions = {}; // filename → description
let customGroups = {}; // filename → groupId (from /groups)
@@ -2510,6 +2527,7 @@
}
if (!map.has(gid)) map.set(gid, []);
map.get(gid).push(f);
fileGroups[f.cleanName] = gid;
}
// Sort each group: by explicit sort_order first, then by filename length
for (const files of map.values()) {
@@ -2573,6 +2591,7 @@
const data = groupData.get(gid);
if (!data) return;
if (lbCurrentGid !== gid) { // clear refs when switching groups
_prevGid = lbCurrentGid;
_sbRefIndices = [];
_sceneRefs = [null, null, null];
_scenePersonInit = false;
@@ -2580,6 +2599,8 @@
stopOrbitFast();
}
lbCurrentGid = gid;
_lastSelectedGid = gid;
localStorage.setItem('lastSelectedGid', gid);
lbUrls = data.urls;
lbNames = data.names;
lbIdx = startIdx !== undefined ? startIdx : (cycleIdx.get(gid) || 0);
@@ -2613,6 +2634,60 @@
openStudio(gid, idx);
}
function jumpToPrevGroup() {
if (_prevGid) openStudio(_prevGid);
}
function formatPadValue(v) {
if (v === undefined || v === null || v === '') return '0.00';
let val = parseFloat(v);
if (isNaN(val)) return '0.00';
const s = v.toString();
// User requested: show 0.00 if it contains a decimal see it as percentage.
// if integer and its greater than 10 its pixel value
if (s.includes('.') || s.includes('%')) return val.toFixed(2);
if (val > 10) return val.toString();
return val.toFixed(2);
}
function updatePadPreview() {
const viewer = document.getElementById('studioViewer');
if (!viewer) return;
// Clean up existing preview overlays
viewer.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
const isManual = !!document.getElementById('padBar');
const getV = (side) => {
const el = document.getElementById(isManual ? 'pad' + side : 'sbPad' + side);
return el ? el.value : '0';
};
const sides = ['Left', 'Right'];
sides.forEach(side => {
const vStr = getV(side);
if (!vStr || vStr === '0' || vStr === '0.00') return;
const val = parseFloat(vStr);
if (isNaN(val) || val <= 0) return;
const isPercent = vStr.includes('%') || vStr.includes('.');
const width = isPercent ? (val + '%') : (val + 'px');
const overlay = document.createElement('div');
overlay.className = 'pad-preview-overlay';
overlay.style.top = '0';
overlay.style.bottom = '0';
if (side === 'Left') {
overlay.style.left = '0';
overlay.style.width = width;
} else {
overlay.style.right = '0';
overlay.style.width = width;
}
viewer.appendChild(overlay);
});
}
function closeStudio() {
document.getElementById('studio').classList.remove('open');
document.body.style.overflow = '';
@@ -2749,6 +2824,9 @@
lbImgEl.src = lbUrls[lbIdx];
}
const backBtn = document.getElementById('lbBackBtn');
if (backBtn) backBtn.style.display = _prevGid ? '' : 'none';
const groupNameInput = document.getElementById('lbGroupNameInput');
groupNameInput.value = groupNames[lbCurrentGid] || '';
@@ -3853,10 +3931,13 @@
const bar = document.createElement('div');
bar.id = 'padBar';
bar.style.cssText = 'position:absolute;top:0;left:0;right:0;display:flex;gap:8px;padding:8px;background:rgba(0,0,0,0.85);z-index:101;justify-content:flex-end;align-items:center;flex-wrap:wrap;';
const numInput = (id, label) =>
`<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`;
const numInput = (id, label) => {
const onInput = `updatePadPreview()`;
const onBlur = `this.value = formatPadValue(this.value); updatePadPreview()`;
return `<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#ccc">${label}<input type="text" id="${id}" value="0.00" oninput="${onInput}" onblur="${onBlur}" style="width:52px;background:#111;border:1px solid #333;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right" placeholder="px or %"></label>`;
};
bar.innerHTML =
`<span style="flex:1;font-size:11px;color:#aaa">Pad canvas (px):</span>`
`<span style="flex:1;font-size:11px;color:#aaa">Pad canvas (px or %):</span>`
+ numInput('padTop','↑')
+ numInput('padRight','→')
+ numInput('padBottom','↓')
@@ -3873,10 +3954,13 @@
+ `<button class="sb-btn" onclick="cancelManualPad()">Cancel</button>`
+ `<button class="sb-btn primary" id="padConfirmBtn" onclick="confirmManualPad()">Pad</button>`;
viewer.appendChild(bar);
updatePadPreview();
}
function cancelManualPad() {
document.getElementById('padBar')?.remove();
// Clean up preview overlays
document.querySelectorAll('.pad-preview-overlay').forEach(el => el.remove());
}
async function confirmManualPad() {
@@ -4880,7 +4964,7 @@
: `<img src="${firstVisUrl}" alt="${label}" loading="${isRecent ? 'eager' : 'lazy'}" onerror="this.style.opacity='0.2'">`;
return `
<div class="image-card ${isNew ? 'new' : ''}" data-group="${safeGid}" onclick="handleCardClick(this)">
<div class="image-card ${isNew ? 'new' : ''} ${_lastSelectedGid === gid ? 'highlight-prev' : ''}" data-group="${safeGid}" onclick="handleCardClick(this)">
<div class="image-actions">
${!isVideoCard ? `<div class="action-btn" title="Remove Background for Group" onclick="removeBgGroup(this, event)">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -5435,6 +5519,11 @@
if ((e.key === 'p' || e.key === 'P') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
togglePrivacyMode(); return;
}
if ((e.key === 'a' || e.key === 'A') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (document.getElementById('studio').classList.contains('open')) {
startManualPad(); return;
}
}
if ((e.key === 'g' || e.key === 'G') && tag !== 'INPUT' && tag !== 'TEXTAREA') {
if (_activeSidebarTab === 'generate') {
const btn = document.getElementById('sbGenBtn');
@@ -5831,10 +5920,12 @@
<span style="font-size:11px;color:#666" title="Expand canvas before generation — gives the model space to outpaint">Pad:</span>
${['Top','Right','Bottom','Left'].map(side => {
const val = side==='Top'?_sbPadTop:side==='Right'?_sbPadRight:side==='Bottom'?_sbPadBottom:_sbPadLeft;
const onInput = `updateSbGenBtn(); updatePadPreview()`;
const onBlur = `this.value = formatPadValue(this.value); ${onInput}`;
return `<label style="display:flex;align-items:center;gap:2px;font-size:11px;color:#888">${side[0]}
<input type="text" id="sbPad${side}" value="${val}" placeholder="px or %"
style="width:48px;background:#111;border:1px solid #2a2a2a;color:#ccc;border-radius:4px;padding:2px 4px;font-size:11px;text-align:right"
oninput="updateSbGenBtn()"></label>`;
oninput="${onInput}" onblur="${onBlur}"></label>`;
}).join('')}
<label style="display:flex;align-items:center;gap:3px;font-size:11px;color:#888;cursor:pointer" title="Instruct the model to fill in the padded area">
<input type="checkbox" id="sbPadOutpaint" ${(_sbPadOutpaint??false)?'checked':''} onchange="updateSbGenBtn()" style="cursor:pointer">Outpaint</label>
@@ -6087,7 +6178,13 @@
bar.innerHTML = overlay.map(a => {
const pSafe = a.prompt.replace(/'/g, "\\'");
return `<button onclick="submitSingleAngle('${pSafe}')" title="${escHtml(a.name)}">${a.icon}</button>`;
}).join('');
}).join('') + `
<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"/>
<path d="M12 8v8m-4-4h8"/>
</svg>
</button>`;
}
function updateSbGenBtn() {