931 lines
34 KiB
HTML
931 lines
34 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Live Image Monitor</title>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: #0f0f0f;
|
|
color: #e0e0e0;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.header {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0;
|
|
background: rgba(15, 15, 15, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border-bottom: 1px solid #333;
|
|
padding: 16px 24px;
|
|
z-index: 100;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-size: 13px;
|
|
color: #888;
|
|
}
|
|
|
|
.status-dot {
|
|
width: 8px; height: 8px;
|
|
border-radius: 50%;
|
|
background: #22c55e;
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.4; }
|
|
}
|
|
|
|
.status-dot.updating {
|
|
background: #f59e0b;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn {
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
color: #ccc;
|
|
padding: 6px 14px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.btn:hover {
|
|
background: #252525;
|
|
border-color: #444;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn.primary {
|
|
background: #2563eb;
|
|
border-color: #2563eb;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn.primary:hover {
|
|
background: #3b82f6;
|
|
}
|
|
|
|
.prompt-bar {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.prompt-input {
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
color: #e0e0e0;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
width: 320px;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.prompt-input:focus {
|
|
border-color: #2563eb;
|
|
}
|
|
|
|
.prompt-input::placeholder {
|
|
color: #555;
|
|
}
|
|
|
|
.count {
|
|
font-size: 13px;
|
|
color: #666;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.gallery {
|
|
padding: 100px 24px 40px;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
max-width: 1800px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.image-card {
|
|
background: #1a1a1a;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 12px;
|
|
overflow: hidden;
|
|
transition: transform 0.2s, border-color 0.2s;
|
|
position: relative;
|
|
}
|
|
|
|
.image-card:hover {
|
|
transform: translateY(-2px);
|
|
border-color: #444;
|
|
}
|
|
|
|
.image-card.new {
|
|
border-color: #2563eb;
|
|
animation: highlight 1s ease;
|
|
}
|
|
|
|
@keyframes highlight {
|
|
from { box-shadow: 0 0 0 2px #2563eb; }
|
|
to { box-shadow: none; }
|
|
}
|
|
|
|
.image-wrapper {
|
|
position: relative;
|
|
padding-top: 300%; /* 1:3 aspect ratio (3x taller than wide) */
|
|
background: #111;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.image-wrapper img {
|
|
position: absolute;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
object-fit: cover;
|
|
transition: transform 0.3s;
|
|
}
|
|
|
|
.image-card:hover .image-wrapper img {
|
|
transform: scale(1.03);
|
|
}
|
|
|
|
.image-info {
|
|
padding: 14px 16px;
|
|
}
|
|
|
|
.image-name {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: #fff;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.image-meta {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.image-time {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.badge {
|
|
background: #2563eb;
|
|
color: #fff;
|
|
font-size: 10px;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.badge.recent {
|
|
background: #22c55e;
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 120px 20px;
|
|
color: #555;
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 64px; height: 64px;
|
|
margin-bottom: 16px;
|
|
opacity: 0.3;
|
|
}
|
|
|
|
.empty-state h2 {
|
|
font-size: 20px;
|
|
color: #777;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.empty-state p {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.toast {
|
|
position: fixed;
|
|
bottom: 24px; right: 24px;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
padding: 12px 20px;
|
|
border-radius: 8px;
|
|
font-size: 13px;
|
|
color: #ccc;
|
|
transform: translateY(100px);
|
|
opacity: 0;
|
|
transition: all 0.3s;
|
|
z-index: 200;
|
|
}
|
|
|
|
.toast.show {
|
|
transform: translateY(0);
|
|
opacity: 1;
|
|
}
|
|
|
|
.toast.success { border-left: 3px solid #22c55e; }
|
|
.toast.info { border-left: 3px solid #2563eb; }
|
|
|
|
.btn.active {
|
|
background: #2563eb;
|
|
border-color: #2563eb;
|
|
color: #fff;
|
|
}
|
|
|
|
.image-card.selectable {
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
|
|
.image-card.selected {
|
|
border-color: #2563eb;
|
|
box-shadow: 0 0 0 2px rgba(37,99,235,0.35);
|
|
}
|
|
|
|
.select-indicator {
|
|
position: absolute;
|
|
top: 8px; right: 8px;
|
|
width: 22px; height: 22px;
|
|
border-radius: 50%;
|
|
border: 2px solid rgba(255,255,255,0.5);
|
|
background: rgba(0,0,0,0.45);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 10;
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
color: #fff;
|
|
transition: background 0.15s, border-color 0.15s;
|
|
}
|
|
|
|
.selectable .select-indicator { display: flex; }
|
|
|
|
.selected .select-indicator {
|
|
background: #2563eb;
|
|
border-color: #2563eb;
|
|
}
|
|
|
|
.selected .select-indicator::after { content: '✓'; }
|
|
|
|
.batch-bar {
|
|
position: fixed;
|
|
bottom: 0; left: 0; right: 0;
|
|
background: rgba(12,12,12,0.97);
|
|
backdrop-filter: blur(12px);
|
|
border-top: 1px solid #2a2a2a;
|
|
padding: 14px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
z-index: 100;
|
|
transform: translateY(100%);
|
|
transition: transform 0.25s ease;
|
|
}
|
|
|
|
.batch-bar.visible { transform: translateY(0); }
|
|
|
|
.batch-count {
|
|
font-size: 13px;
|
|
color: #888;
|
|
min-width: 80px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.batch-prompt-wrap { flex: 1; position: relative; }
|
|
|
|
.batch-prompt {
|
|
width: 100%;
|
|
background: #1a1a1a;
|
|
border: 1px solid #333;
|
|
color: #e0e0e0;
|
|
padding: 7px 12px;
|
|
border-radius: 6px;
|
|
font-size: 13px;
|
|
outline: none;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.batch-prompt:focus { border-color: #2563eb; }
|
|
.batch-prompt::placeholder { color: #555; }
|
|
|
|
.batch-progress {
|
|
font-size: 12px;
|
|
color: #666;
|
|
min-width: 90px;
|
|
text-align: right;
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="header">
|
|
<h1>
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<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>
|
|
Live Image Monitor
|
|
</h1>
|
|
<div class="prompt-bar">
|
|
<input class="prompt-input" id="promptInput" type="text" placeholder="prompt..." onkeydown="if(event.key==='Enter')setPrompt()" />
|
|
<button class="btn primary" onclick="setPrompt()">Set Prompt</button>
|
|
</div>
|
|
<div class="controls">
|
|
<span class="count" id="count">0 images</span>
|
|
<button class="btn" onclick="setIntervalTime(30)">30s</button>
|
|
<button class="btn" onclick="setIntervalTime(120)">2m</button>
|
|
<button class="btn primary" onclick="refreshNow()">Refresh Now</button>
|
|
<button class="btn" id="selectBtn" onclick="toggleSelectMode()">Select</button>
|
|
</div>
|
|
<div class="status">
|
|
<span class="status-dot" id="statusDot"></span>
|
|
<span id="statusText">Auto-refresh: 2m</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="gallery" id="gallery">
|
|
<div class="empty-state">
|
|
<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 this HTML file in your image folder, or configure the path below.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast" id="toast"></div>
|
|
|
|
<div class="batch-bar" id="batchBar">
|
|
<span class="batch-count" id="batchCount">0 selected</span>
|
|
<div class="batch-prompt-wrap">
|
|
<input class="batch-prompt" id="batchPromptInput" list="promptHistory"
|
|
placeholder="temporal prompt..."
|
|
onkeydown="if(event.key==='Enter')processSelected()" />
|
|
<datalist id="promptHistory"></datalist>
|
|
</div>
|
|
<button class="btn" onclick="selectAll()">All</button>
|
|
<button class="btn" onclick="deselectAll()">None</button>
|
|
<button class="btn primary" id="processBtn" onclick="processSelected()">Process</button>
|
|
<span class="batch-progress" id="batchProgress"></span>
|
|
<button class="btn" onclick="toggleSelectMode()">Done</button>
|
|
</div>
|
|
|
|
<script>
|
|
// ============================================
|
|
// CONFIGURATION - EDIT THIS SECTION
|
|
// ============================================
|
|
|
|
// Set this to your image folder path relative to this HTML file
|
|
// Examples: './' (same folder), './images/', '../screenshots/'
|
|
const IMAGE_FOLDER = './';
|
|
|
|
// --- HYDRATION_START ---
|
|
const PRELOADED_IMAGES = [
|
|
"20260617_215245_img_87.png",
|
|
"20260617_134615_img_86.png",
|
|
"20260617_134229_img_83.png",
|
|
"20260617_134119_img_85.png",
|
|
"20260617_134041_img_84.png",
|
|
"20260617_133917_img_82.png",
|
|
"20260617_133832_img_81.png",
|
|
"20260617_133519_img_80.png",
|
|
"20260617_133411_img_79.png",
|
|
"20260617_133154_img_78.png",
|
|
"20260617_133129_img_77.png",
|
|
"20260617_132851_img_76.png",
|
|
"20260617_074613_img_75.png",
|
|
"20260617_074556_img_74.png",
|
|
"20260617_074413_img_73.png",
|
|
"20260617_074223_img_72.png",
|
|
"20260617_015946_img_71.png",
|
|
"20260617_015728_img_70.png",
|
|
"20260617_015611_img_69.png",
|
|
"20260617_015007_img_68.png",
|
|
"20260617_014351_img_66.png",
|
|
"20260617_013327_img_67.png",
|
|
"20260617_013231_img_68.png",
|
|
"20260617_013211_img_65.png",
|
|
"20260617_013150_img_66.png",
|
|
"20260617_013111_img_63.png",
|
|
"20260617_013035_img_64.png",
|
|
"20260617_012709_img_62.png",
|
|
"20260617_011132_img_61.png",
|
|
"20260617_005512_img_60.png",
|
|
"20260617_005200_img_59.png",
|
|
"20260617_005040_img_56.png",
|
|
"20260617_005026_img_55.png",
|
|
"20260617_005008_img_54.png",
|
|
"20260617_004942_img_53.png",
|
|
"20260617_004814_img_57.png",
|
|
"20260616_023306_img_52.png",
|
|
"20260616_023209_img_51.png",
|
|
"20260616_022543_img_50.png",
|
|
"20260616_022349_img_48.png",
|
|
"20260616_021938_20160903_200935.jpg",
|
|
"20260616_021235_img_47.png",
|
|
"20260616_021214_img_46.png",
|
|
"20260616_021150_img_44.png",
|
|
"20260616_021116_img_43.png",
|
|
"20260616_021056_img_45.png",
|
|
"20260616_021003_img_41.png",
|
|
"20260616_020908_img_40.png",
|
|
"20260616_020403_img_39.png",
|
|
"20260616_020059_img_38.png",
|
|
"20260616_020035_img_36.png",
|
|
"20260616_020020_img_35.png",
|
|
"20260616_015949_img_37.png",
|
|
"20260616_015919_img_33.png",
|
|
"20260616_015850_img_34.png",
|
|
"20260616_015341_img_32.png",
|
|
"20260616_014757_img_31.png",
|
|
"20260616_014225_img_30.png",
|
|
"20260616_014057_img_29.png",
|
|
"20260616_013755_img_28.png",
|
|
"20260616_013603_img_27.png",
|
|
"20260616_013013_img_26.png",
|
|
"20260616_012939_img_25.png",
|
|
"20260616_011823_imgxxxx.png",
|
|
"20260616_011447_img_24.png",
|
|
"20260616_010228_img_22.png",
|
|
"20260616_005752_img_21.png",
|
|
"20260616_005727_img_19.png",
|
|
"20260616_005202_img_20.png",
|
|
"20260616_004250_img_18.png",
|
|
"20260616_004220_img_17.png",
|
|
"20260616_004001_img_16.png",
|
|
"20260616_003916_img_15.png",
|
|
"20260616_003803_img_14.png",
|
|
"20260616_003629_img_13.png",
|
|
"20260616_003548_img.png",
|
|
"20260616_002456_test123.jpeg",
|
|
"20260616_002302_image.png",
|
|
"20260615_155756_img_6v1.png",
|
|
"20260615_155354_others.jpeg",
|
|
"20260615_154852_other.jpeg",
|
|
"20260615_154333_other.jpeg",
|
|
"20260615_153749_img_11.png",
|
|
"20260615_153426_img_12.png",
|
|
"20260615_153125_img_10.png",
|
|
"20260615_152826_img.png",
|
|
"20260615_152252_imgxxx.png",
|
|
"20260615_151829_img_92.png",
|
|
"20260615_151614_img_93.png",
|
|
"20260615_150812_img_19_2.png",
|
|
"20260615_150340_test.png",
|
|
"20260615_145017_img_93.png",
|
|
"img_9.png",
|
|
"b1.png",
|
|
"jb3.png",
|
|
"jb.png",
|
|
"jb1.png",
|
|
"img.png",
|
|
"out7.png",
|
|
"out5.png",
|
|
"out4.png",
|
|
"out3.png",
|
|
"out2.png",
|
|
"out.png",
|
|
"bb_01.png",
|
|
"tp236b.png",
|
|
"pass-1.png",
|
|
"t159zr-1.png",
|
|
"kbk99v.png",
|
|
"kk563t.png",
|
|
"Pasted image (9).png",
|
|
"Pasted image (7).png",
|
|
"Pasted image (5).png",
|
|
"Pasted image (4).png",
|
|
"Pasted image (3).png",
|
|
"Pasted image (2).png",
|
|
"Pasted image.png",
|
|
"pa01.png",
|
|
"pa0.png",
|
|
"p13.png",
|
|
"p12-2.png",
|
|
"p12.png",
|
|
"p11.png",
|
|
"p10.png",
|
|
"p1.png",
|
|
"img_2.png",
|
|
"img_3.png"
|
|
];
|
|
// --- HYDRATION_END ---
|
|
|
|
// Supported image extensions
|
|
const EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.svg'];
|
|
|
|
// Auto-refresh interval in seconds (default: 120 = 2 minutes)
|
|
let REFRESH_INTERVAL = 120;
|
|
|
|
// ============================================
|
|
|
|
let autoRefreshTimer = null;
|
|
let knownFiles = new Set();
|
|
let currentFiles = [];
|
|
|
|
function showToast(message, type = 'info') {
|
|
const toast = document.getElementById('toast');
|
|
toast.textContent = message;
|
|
toast.className = `toast ${type} show`;
|
|
setTimeout(() => toast.classList.remove('show'), 3000);
|
|
}
|
|
|
|
function formatTime(date) {
|
|
const now = new Date();
|
|
const diff = Math.floor((now - date) / 1000);
|
|
|
|
if (diff < 60) return 'Just now';
|
|
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`;
|
|
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`;
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
|
|
// Try to get file info using HEAD requests (works with some local servers)
|
|
async function getFileInfo(filename) {
|
|
try {
|
|
const response = await fetch(IMAGE_FOLDER + filename, { method: 'HEAD' });
|
|
const lastMod = response.headers.get('last-modified');
|
|
const size = response.headers.get('content-length');
|
|
return {
|
|
modified: lastMod ? new Date(lastMod) : new Date(),
|
|
size: size ? parseInt(size) : 0
|
|
};
|
|
} catch {
|
|
return { modified: new Date(), size: 0 };
|
|
}
|
|
}
|
|
|
|
async function scanFolder() {
|
|
// For local file:// protocol, we can't list directories.
|
|
// This works when served via HTTP or you can manually list files.
|
|
|
|
// METHOD 1: If you have a simple file server, it might return directory listing
|
|
try {
|
|
const response = await fetch(IMAGE_FOLDER);
|
|
const text = await response.text();
|
|
|
|
// Try to parse common directory listing formats
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(text, 'text/html');
|
|
const links = Array.from(doc.querySelectorAll('a'))
|
|
.map(a => a.getAttribute('href'))
|
|
.filter(href => href && EXTENSIONS.some(ext => href.toLowerCase().endsWith(ext)))
|
|
.map(href => href.split('/').pop()); // Just the filename
|
|
|
|
if (links.length > 0) return links;
|
|
} catch (e) {
|
|
// Directory listing not available
|
|
}
|
|
|
|
// METHOD 2: Manual file list (fallback)
|
|
// If you can't use a server, list your files here or use the manual input below
|
|
return null;
|
|
}
|
|
|
|
async function loadImages() {
|
|
const gallery = document.getElementById('gallery');
|
|
const statusDot = document.getElementById('statusDot');
|
|
statusDot.classList.add('updating');
|
|
|
|
let files = null;
|
|
|
|
// Use preloaded images if available
|
|
if (typeof PRELOADED_IMAGES !== 'undefined' && PRELOADED_IMAGES.length > 0) {
|
|
files = PRELOADED_IMAGES;
|
|
} else {
|
|
files = await scanFolder();
|
|
}
|
|
|
|
// FALLBACK: If auto-scan doesn't work, use this manual approach
|
|
if (!files) {
|
|
files = await discoverImages();
|
|
}
|
|
|
|
// Sort by newest first (we use a cache-busting parameter to force reload)
|
|
const fileObjects = files.map(name => ({
|
|
name: name,
|
|
url: IMAGE_FOLDER + name + '?t=' + Date.now(),
|
|
cleanName: name.split('?')[0]
|
|
}));
|
|
|
|
// Check for new files
|
|
const newFiles = fileObjects.filter(f => !knownFiles.has(f.cleanName));
|
|
newFiles.forEach(f => knownFiles.add(f.cleanName));
|
|
|
|
currentFiles = fileObjects;
|
|
|
|
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 in "${IMAGE_FOLDER}"</h2>
|
|
<p>Make sure this HTML file is in the same folder as your images,<br>or update the IMAGE_FOLDER path in the script.</p>
|
|
</div>
|
|
`;
|
|
statusDot.classList.remove('updating');
|
|
return;
|
|
}
|
|
|
|
// Render gallery
|
|
gallery.innerHTML = fileObjects.map((file, index) => {
|
|
const isNew = index < newFiles.length;
|
|
const isRecent = index < 3;
|
|
|
|
return `
|
|
<div class="image-card ${isNew ? 'new' : ''}" data-name="${file.cleanName}">
|
|
<div class="image-wrapper">
|
|
<div class="select-indicator"></div>
|
|
<img src="${file.url}"
|
|
alt="${file.cleanName}"
|
|
loading="lazy"
|
|
onerror="this.parentElement.innerHTML='<div style=\\'position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#444;font-size:13px;\\'>Failed to load</div>'">
|
|
</div>
|
|
<div class="image-info">
|
|
<div class="image-name" title="${file.cleanName}">${file.cleanName}</div>
|
|
<div class="image-meta">
|
|
<span class="image-time">#${String(fileObjects.length - index).padStart(3, '0')}</span>
|
|
${isRecent ? '<span class="badge recent">Latest</span>' : ''}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
updateCardSelection();
|
|
|
|
document.getElementById('count').textContent = `${fileObjects.length} image${fileObjects.length !== 1 ? 's' : ''}`;
|
|
|
|
if (newFiles.length > 0) {
|
|
showToast(`${newFiles.length} new image${newFiles.length !== 1 ? 's' : ''} detected`, 'success');
|
|
}
|
|
|
|
statusDot.classList.remove('updating');
|
|
}
|
|
|
|
// Discover images by trying common patterns (fallback for file:// protocol)
|
|
async function discoverImages() {
|
|
const found = [];
|
|
|
|
// Try to find images by testing if they exist
|
|
// This is a best-effort approach for local file access
|
|
|
|
// If you know your naming pattern, add it here:
|
|
const patterns = [];
|
|
|
|
// Try to extract from page if there are any references
|
|
const images = document.querySelectorAll('img[src]');
|
|
images.forEach(img => {
|
|
const src = img.getAttribute('src');
|
|
if (src && EXTENSIONS.some(ext => src.toLowerCase().includes(ext))) {
|
|
found.push(src.split('/').pop().split('?')[0]);
|
|
}
|
|
});
|
|
|
|
// Remove duplicates
|
|
return [...new Set(found)];
|
|
}
|
|
|
|
function setIntervalTime(seconds) {
|
|
REFRESH_INTERVAL = seconds;
|
|
document.getElementById('statusText').textContent = `Auto-refresh: ${seconds < 60 ? seconds + 's' : (seconds / 60) + 'm'}`;
|
|
|
|
clearInterval(autoRefreshTimer);
|
|
autoRefreshTimer = setInterval(refreshNow, REFRESH_INTERVAL * 1000);
|
|
|
|
showToast(`Refresh interval set to ${seconds < 60 ? seconds + ' seconds' : (seconds / 60) + ' minutes'}`, 'info');
|
|
}
|
|
|
|
function refreshNow() {
|
|
loadImages();
|
|
}
|
|
|
|
const API = 'http://127.0.0.1:8500';
|
|
|
|
// --- selection state ---
|
|
let selectionMode = false;
|
|
const selectedFiles = new Set();
|
|
|
|
function toggleSelectMode() {
|
|
selectionMode = !selectionMode;
|
|
if (!selectionMode) selectedFiles.clear();
|
|
document.getElementById('selectBtn').classList.toggle('active', selectionMode);
|
|
const bar = document.getElementById('batchBar');
|
|
bar.classList.toggle('visible', selectionMode);
|
|
document.querySelector('.gallery').style.paddingBottom = selectionMode ? '90px' : '40px';
|
|
updateCardSelection();
|
|
updateBatchBar();
|
|
}
|
|
|
|
function toggleFile(name) {
|
|
if (selectedFiles.has(name)) selectedFiles.delete(name);
|
|
else selectedFiles.add(name);
|
|
updateCardSelection();
|
|
updateBatchBar();
|
|
}
|
|
|
|
function updateCardSelection() {
|
|
document.querySelectorAll('.image-card').forEach(card => {
|
|
const name = card.dataset.name;
|
|
card.classList.toggle('selectable', selectionMode);
|
|
card.classList.toggle('selected', selectionMode && selectedFiles.has(name));
|
|
});
|
|
}
|
|
|
|
function updateBatchBar() {
|
|
const n = selectedFiles.size;
|
|
document.getElementById('batchCount').textContent = `${n} selected`;
|
|
document.getElementById('processBtn').textContent = n > 0 ? `Process (${n})` : 'Process';
|
|
}
|
|
|
|
function selectAll() {
|
|
currentFiles.forEach(f => selectedFiles.add(f.cleanName));
|
|
updateCardSelection();
|
|
updateBatchBar();
|
|
}
|
|
|
|
function deselectAll() {
|
|
selectedFiles.clear();
|
|
updateCardSelection();
|
|
updateBatchBar();
|
|
}
|
|
|
|
// --- batch processing ---
|
|
let pollTimer = null;
|
|
|
|
async function processSelected() {
|
|
const prompt = document.getElementById('batchPromptInput').value.trim();
|
|
if (!prompt) { showToast('Enter a prompt first', 'info'); return; }
|
|
if (selectedFiles.size === 0) { showToast('No images selected', 'info'); return; }
|
|
|
|
savePromptHistory(prompt);
|
|
const filenames = [...selectedFiles];
|
|
document.getElementById('batchProgress').textContent = 'Submitting…';
|
|
|
|
try {
|
|
const r = await fetch(`${API}/batch`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ filenames, prompt, seed: -1, max_area: 0 }),
|
|
});
|
|
if (!r.ok) { showToast('Batch failed to start', 'info'); document.getElementById('batchProgress').textContent = ''; return; }
|
|
const { job_id, total } = await r.json();
|
|
showToast(`Processing ${total} image${total !== 1 ? 's' : ''}…`, 'info');
|
|
pollJob(job_id, total);
|
|
} catch (e) {
|
|
showToast('API not reachable', 'info');
|
|
document.getElementById('batchProgress').textContent = '';
|
|
}
|
|
}
|
|
|
|
function pollJob(job_id, total) {
|
|
clearTimeout(pollTimer);
|
|
pollTimer = setTimeout(async () => {
|
|
try {
|
|
const r = await fetch(`${API}/batch/${job_id}`);
|
|
if (!r.ok) return;
|
|
const job = await r.json();
|
|
const done = job.done + job.failed;
|
|
document.getElementById('batchProgress').textContent = `${done}/${total}`;
|
|
if (job.status === 'running') {
|
|
pollJob(job_id, total);
|
|
} else {
|
|
const msg = job.failed > 0
|
|
? `Done: ${job.done} ok, ${job.failed} failed`
|
|
: `Done: ${job.done} processed`;
|
|
document.getElementById('batchProgress').textContent = msg;
|
|
showToast(msg, 'success');
|
|
loadImages();
|
|
}
|
|
} catch (e) { /* ignore transient errors */ pollJob(job_id, total); }
|
|
}, 2000);
|
|
}
|
|
|
|
// --- prompt history (localStorage) ---
|
|
function loadPromptHistory() {
|
|
const hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
|
|
const dl = document.getElementById('promptHistory');
|
|
dl.innerHTML = hist.map(p => `<option value="${p.replace(/"/g, '"')}"></option>`).join('');
|
|
}
|
|
|
|
function savePromptHistory(prompt) {
|
|
let hist = JSON.parse(localStorage.getItem('batchPromptHistory') || '[]');
|
|
hist = [prompt, ...hist.filter(p => p !== prompt)].slice(0, 20);
|
|
localStorage.setItem('batchPromptHistory', JSON.stringify(hist));
|
|
loadPromptHistory();
|
|
}
|
|
|
|
async function loadCurrentPrompt() {
|
|
try {
|
|
const r = await fetch(`${API}/config`);
|
|
if (r.ok) {
|
|
const conf = await r.json();
|
|
document.getElementById('promptInput').value = conf.prompt || '';
|
|
}
|
|
} catch (e) {
|
|
// API not reachable yet
|
|
}
|
|
}
|
|
|
|
async function setPrompt() {
|
|
const prompt = document.getElementById('promptInput').value.trim();
|
|
if (!prompt) return;
|
|
try {
|
|
const r = await fetch(`${API}/config`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ prompt }),
|
|
});
|
|
if (r.ok) {
|
|
showToast('Prompt updated', 'success');
|
|
} else {
|
|
showToast('Failed to update prompt', 'info');
|
|
}
|
|
} catch (e) {
|
|
showToast('API not reachable', 'info');
|
|
}
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadCurrentPrompt();
|
|
loadPromptHistory();
|
|
loadImages();
|
|
autoRefreshTimer = setInterval(refreshNow, REFRESH_INTERVAL * 1000);
|
|
|
|
// Selection click delegation
|
|
document.getElementById('gallery').addEventListener('click', e => {
|
|
if (!selectionMode) return;
|
|
const card = e.target.closest('.image-card');
|
|
if (card) toggleFile(card.dataset.name);
|
|
});
|
|
});
|
|
|
|
// Handle visibility change - refresh when tab becomes active
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (!document.hidden) {
|
|
loadImages();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |