add generate-viewer - helper for work
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DJI Returns — Image Reference | CMS Distribution</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html,body{height:100%;overflow:hidden}
|
||||
body{font-family:"Segoe UI",Calibri,Arial,sans-serif;background:#0f1316;color:#c8d8ea;display:flex;flex-direction:column}
|
||||
|
||||
/* ── Header ── */
|
||||
.header{
|
||||
flex-shrink:0;height:54px;display:flex;align-items:center;gap:20px;
|
||||
padding:0 22px;z-index:10;
|
||||
background:linear-gradient(135deg,#14191b 0%,#0e1e28 60%,#0b2020 100%);
|
||||
border-bottom:2px solid transparent;
|
||||
border-image:linear-gradient(to right,#3eafff,#2acc94) 1;
|
||||
}
|
||||
.brand-name{font-size:18px;font-weight:800;letter-spacing:.3px;color:#fff}
|
||||
.brand-name em{
|
||||
font-style:normal;
|
||||
background:linear-gradient(90deg,#3eafff,#2acc94);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text
|
||||
}
|
||||
.brand-pipe{color:#2a3a3a;font-size:13px;margin:0 2px}
|
||||
.brand-sub{font-size:12px;color:#4a6878}
|
||||
.header-right{margin-left:auto;display:flex;align-items:center;gap:12px}
|
||||
.stat-chip{
|
||||
background:rgba(62,175,255,.08);border:1px solid rgba(62,175,255,.18);
|
||||
border-radius:20px;padding:3px 12px;font-size:11px;color:#5a8898;white-space:nowrap
|
||||
}
|
||||
.stat-chip strong{color:#2acc94}
|
||||
|
||||
/* ── Layout ── */
|
||||
.layout{display:flex;flex:1;min-height:0;overflow:hidden}
|
||||
|
||||
/* ── Sidebar ── */
|
||||
.sidebar{
|
||||
width:230px;flex-shrink:0;overflow-y:scroll;
|
||||
background:#0c1014;border-right:1px solid #1a2428;display:flex;flex-direction:column
|
||||
}
|
||||
.sidebar-section{padding:16px 0 4px}
|
||||
.sidebar-section-label{
|
||||
padding:0 14px 5px;font-size:9px;font-weight:700;
|
||||
letter-spacing:2px;text-transform:uppercase;color:#2a4048
|
||||
}
|
||||
.nav-item{
|
||||
display:flex;align-items:center;gap:8px;padding:8px 14px;
|
||||
cursor:pointer;border-left:3px solid transparent;
|
||||
font-size:12px;color:#6a8898;
|
||||
transition:background .1s,color .1s;user-select:none
|
||||
}
|
||||
.nav-item:hover{background:#111820;color:#a0c0d0}
|
||||
.nav-item.active{
|
||||
background:#0d181e;
|
||||
border-left:3px solid #2acc94;
|
||||
color:#d0e8f0;font-weight:600
|
||||
}
|
||||
.nav-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
|
||||
.nav-all-dot{width:18px;height:7px;border-radius:4px;flex-shrink:0;background:linear-gradient(90deg,#3eafff,#2acc94)}
|
||||
.nav-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.nav-badge{background:#141e24;border-radius:12px;padding:1px 7px;font-size:10px;color:#3a5060;flex-shrink:0}
|
||||
.nav-item.active .nav-badge{background:#0d2030;color:#3eafff}
|
||||
|
||||
/* ── Main ── */
|
||||
.main{flex:1;display:flex;flex-direction:column;min-width:0;overflow:hidden}
|
||||
|
||||
/* ── Toolbar ── */
|
||||
.toolbar{
|
||||
flex-shrink:0;padding:10px 16px;
|
||||
background:#0c1014;border-bottom:1px solid #1a2428;
|
||||
display:flex;align-items:center;gap:10px
|
||||
}
|
||||
.search-wrap{flex:1;position:relative}
|
||||
.search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);color:#2a4048;font-size:14px;pointer-events:none}
|
||||
.search{
|
||||
width:100%;background:#111820;border:1px solid #1a2a30;border-radius:6px;
|
||||
padding:7px 32px 7px 32px;color:#c0d8e8;font-size:13px;outline:none;transition:border-color .15s
|
||||
}
|
||||
.search:focus{border-color:#3eafff;background:#0d1820}
|
||||
.search::placeholder{color:#2a3e48}
|
||||
.search-clear{
|
||||
position:absolute;right:8px;top:50%;transform:translateY(-50%);
|
||||
background:none;border:none;color:#2a4048;cursor:pointer;font-size:16px;display:none;line-height:1;padding:2px 4px
|
||||
}
|
||||
.search-clear:hover{color:#3eafff}
|
||||
.toolbar-divider{width:1px;height:24px;background:#1a2428}
|
||||
.view-btns{display:flex;gap:3px}
|
||||
.vbtn{
|
||||
background:#111820;border:1px solid #1a2a30;color:#2a4048;
|
||||
width:32px;height:32px;border-radius:5px;cursor:pointer;font-size:14px;
|
||||
display:flex;align-items:center;justify-content:center;transition:all .1s
|
||||
}
|
||||
.vbtn:hover{background:#141e28;color:#6a8898}
|
||||
.vbtn.active{background:#0d2030;color:#3eafff;border-color:#1a3a50}
|
||||
|
||||
/* ── Category bar ── */
|
||||
.cat-bar{
|
||||
flex-shrink:0;padding:8px 16px;border-bottom:1px solid #1a2428;
|
||||
background:#0c1014;display:flex;align-items:center;gap:10px
|
||||
}
|
||||
.cat-bar-dot{width:9px;height:9px;border-radius:50%;flex-shrink:0}
|
||||
.cat-bar-all{width:22px;height:9px;border-radius:5px;flex-shrink:0;background:linear-gradient(90deg,#3eafff,#2acc94)}
|
||||
.cat-bar-name{font-size:13px;font-weight:700;color:#c0d8e8}
|
||||
.cat-bar-desc{font-size:11px;color:#3a5060}
|
||||
.cat-bar-count{
|
||||
margin-left:auto;font-size:20px;font-weight:800;
|
||||
background:linear-gradient(90deg,#3eafff,#2acc94);
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text
|
||||
}
|
||||
|
||||
/* ── Grid ── */
|
||||
.grid-area{flex:1;overflow-y:scroll;padding:12px}
|
||||
.grid{display:grid;gap:8px}
|
||||
.grid.large {grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
|
||||
.grid.medium{grid-template-columns:repeat(auto-fill,minmax(150px,1fr))}
|
||||
.grid.small {grid-template-columns:repeat(auto-fill,minmax(110px,1fr))}
|
||||
|
||||
/* ── Cards ── */
|
||||
.card{
|
||||
background:#0c1014;border-radius:6px;overflow:hidden;cursor:pointer;
|
||||
border:1px solid #1a2428;transition:transform .12s,border-color .12s,box-shadow .12s
|
||||
}
|
||||
.card:hover{transform:translateY(-2px);box-shadow:0 6px 20px rgba(0,0,0,.5)}
|
||||
.card:hover.accept {border-color:#1a6a50}
|
||||
.card:hover.reject {border-color:#882020}
|
||||
.card:hover.borderline{border-color:#886000}
|
||||
.card:hover.reference{border-color:#1a4a70}
|
||||
.card img{width:100%;height:160px;object-fit:contain;display:block;background:#080b0e;padding:4px}
|
||||
.card-foot{padding:5px 8px;display:flex;align-items:center;gap:5px}
|
||||
.card-dot{width:5px;height:5px;border-radius:50%;flex-shrink:0}
|
||||
.card-label{font-size:10px;color:#3a5060;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1}
|
||||
|
||||
/* ── Empty ── */
|
||||
.empty{text-align:center;padding:80px 20px;color:#2a3a40}
|
||||
.empty-icon{font-size:48px;margin-bottom:12px;opacity:.4}
|
||||
.empty-text{font-size:15px}
|
||||
|
||||
/* ── Lightbox ── */
|
||||
.lb{display:none;position:fixed;inset:0;background:rgba(4,7,10,.96);z-index:1000;flex-direction:column;align-items:center;justify-content:center}
|
||||
.lb.open{display:flex}
|
||||
.lb-header{
|
||||
position:fixed;top:0;left:0;right:0;padding:12px 20px;
|
||||
display:flex;align-items:center;gap:10px;
|
||||
background:rgba(10,16,20,.92);backdrop-filter:blur(8px)
|
||||
}
|
||||
.lb-badge{padding:3px 10px;border-radius:12px;font-size:10px;font-weight:700;letter-spacing:.5px}
|
||||
.lb-filename{font-size:12px;color:#3a5060;flex:1}
|
||||
.lb-close{
|
||||
background:#141e24;border:none;color:#6a8898;width:32px;height:32px;
|
||||
border-radius:50%;cursor:pointer;font-size:18px;display:flex;align-items:center;
|
||||
justify-content:center;transition:background .1s,color .1s
|
||||
}
|
||||
.lb-close:hover{background:#882020;color:#fff}
|
||||
.lb-img-wrap{max-width:90vw;max-height:80vh}
|
||||
.lb-img-wrap img{max-width:90vw;max-height:80vh;object-fit:contain;border-radius:4px;display:block}
|
||||
.lb-footer{
|
||||
position:fixed;bottom:0;left:0;right:0;padding:10px 20px;
|
||||
display:flex;align-items:center;justify-content:center;gap:16px;
|
||||
background:rgba(10,16,20,.92);backdrop-filter:blur(8px)
|
||||
}
|
||||
.lb-nav-btn{
|
||||
background:#111820;border:1px solid #1a2a30;color:#6a8898;
|
||||
padding:7px 20px;border-radius:6px;cursor:pointer;font-size:13px;transition:all .1s
|
||||
}
|
||||
.lb-nav-btn:hover{background:#0d2030;color:#3eafff;border-color:#1a3a50}
|
||||
.lb-counter{font-size:13px;color:#3a5060;min-width:80px;text-align:center}
|
||||
|
||||
/* ── Scrollbars ── */
|
||||
::-webkit-scrollbar{width:6px;height:6px}
|
||||
::-webkit-scrollbar-track{background:#080b0e}
|
||||
::-webkit-scrollbar-thumb{background:#1a2a30;border-radius:3px}
|
||||
::-webkit-scrollbar-thumb:hover{background:#2a3e50}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<div class="brand-name">CMS <em>Distribution</em></div>
|
||||
<span class="brand-pipe">|</span>
|
||||
<div class="brand-sub">DJI Returns — Damage Reference Library</div>
|
||||
<div class="header-right">
|
||||
<div class="stat-chip"><strong id="visCount">0</strong> images showing</div>
|
||||
<div class="stat-chip" id="catChip">All categories</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout">
|
||||
<nav class="sidebar" id="sidebar"></nav>
|
||||
<div class="main">
|
||||
<div class="toolbar">
|
||||
<div class="search-wrap">
|
||||
<span class="search-icon">⚲</span>
|
||||
<input class="search" id="search" type="text"
|
||||
placeholder="Search by category or filename…"
|
||||
oninput="onSearch(this.value)" autocomplete="off">
|
||||
<button class="search-clear" id="searchClear" onclick="clearSearch()">✕</button>
|
||||
</div>
|
||||
<div class="toolbar-divider"></div>
|
||||
<div class="view-btns">
|
||||
<button class="vbtn active" id="vLarge" onclick="setView('large')" title="Large">▮▮</button>
|
||||
<button class="vbtn" id="vMedium" onclick="setView('medium')" title="Medium">▮▮▮</button>
|
||||
<button class="vbtn" id="vSmall" onclick="setView('small')" title="Small">▮▮▮▮</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cat-bar" id="catBar"></div>
|
||||
<div class="grid-area">
|
||||
<div class="grid large" id="grid"></div>
|
||||
<div class="empty" id="empty" style="display:none">
|
||||
<div class="empty-icon">🔍</div>
|
||||
<div class="empty-text">No images match your search.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lb" id="lb">
|
||||
<div class="lb-header">
|
||||
<span class="lb-badge" id="lbBadge"></span>
|
||||
<span class="lb-filename" id="lbFilename"></span>
|
||||
<button class="lb-close" onclick="closeLb()">✕</button>
|
||||
</div>
|
||||
<div class="lb-img-wrap"><img id="lbImg" src="" alt=""></div>
|
||||
<div class="lb-footer">
|
||||
<button class="lb-nav-btn" onclick="lbNav(-1)">← Prev</button>
|
||||
<div class="lb-counter" id="lbCounter"></div>
|
||||
<button class="lb-nav-btn" onclick="lbNav(1)">Next →</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Data (injected by generate_viewer.py) ─────────────────────────────────────
|
||||
const IMAGES = __IMAGE_DATA__;
|
||||
const CATS = __CAT_DATA__;
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────────
|
||||
let activeCat = null, query = '', viewSize = 'large', lbIdx = 0, lbFiltered = [];
|
||||
|
||||
const SECTION_ORDER = ['accept','reject','borderline','reference'];
|
||||
const SECTION_LABELS = {accept:'ACCEPTABLE',reject:'REJECT',borderline:'BORDERLINE',reference:'REFERENCE'};
|
||||
|
||||
// ── Sidebar ───────────────────────────────────────────────────────────────────
|
||||
function buildSidebar() {
|
||||
const sb = document.getElementById('sidebar');
|
||||
const sections = {};
|
||||
CATS.forEach(c => { (sections[c.section] = sections[c.section]||[]).push(c); });
|
||||
|
||||
let html = '<div class="sidebar-section"><div class="sidebar-section-label">OVERVIEW</div>'
|
||||
+ `<div class="nav-item active" data-cat="" onclick="selectCat(null)">
|
||||
<div class="nav-all-dot"></div>
|
||||
<span class="nav-label">All Images</span>
|
||||
<span class="nav-badge">${IMAGES.length}</span>
|
||||
</div></div>`;
|
||||
|
||||
SECTION_ORDER.forEach(sec => {
|
||||
const items = sections[sec];
|
||||
if (!items) return;
|
||||
html += `<div class="sidebar-section"><div class="sidebar-section-label">${SECTION_LABELS[sec]||sec.toUpperCase()}</div>`;
|
||||
items.forEach(c => {
|
||||
const safeKey = c.key.replace(/'/g,"\\'");
|
||||
html += `<div class="nav-item" data-cat="${c.key}" onclick="selectCat('${safeKey}')">
|
||||
<div class="nav-dot" style="background:${c.dot}"></div>
|
||||
<span class="nav-label">${c.label}</span>
|
||||
<span class="nav-badge">${c.count}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
});
|
||||
sb.innerHTML = html;
|
||||
}
|
||||
|
||||
// ── Category bar ──────────────────────────────────────────────────────────────
|
||||
function updateCatBar() {
|
||||
const bar = document.getElementById('catBar');
|
||||
if (activeCat === null) {
|
||||
bar.innerHTML = `<div class="cat-bar-all"></div>
|
||||
<span class="cat-bar-name">All Images</span>
|
||||
<span class="cat-bar-desc">Browse the full library</span>
|
||||
<span class="cat-bar-count">${IMAGES.length}</span>`;
|
||||
} else {
|
||||
const c = CATS.find(x => x.key === activeCat) || {dot:'#888',label:activeCat,desc:'',count:0};
|
||||
bar.innerHTML = `<div class="cat-bar-dot" style="background:${c.dot}"></div>
|
||||
<span class="cat-bar-name">${c.label}</span>
|
||||
<span class="cat-bar-desc">${c.desc}</span>
|
||||
<span class="cat-bar-count">${c.count}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Filtering ─────────────────────────────────────────────────────────────────
|
||||
function getFiltered() {
|
||||
const q = query.toLowerCase();
|
||||
return IMAGES.filter(img => {
|
||||
const catMatch = activeCat === null || img.cat === activeCat;
|
||||
const qMatch = !q || img.path.toLowerCase().includes(q) || img.cat.toLowerCase().includes(q);
|
||||
return catMatch && qMatch;
|
||||
});
|
||||
}
|
||||
|
||||
// ── Render grid ───────────────────────────────────────────────────────────────
|
||||
function renderGrid() {
|
||||
const filtered = getFiltered();
|
||||
lbFiltered = filtered;
|
||||
const grid = document.getElementById('grid');
|
||||
const empty = document.getElementById('empty');
|
||||
document.getElementById('visCount').textContent = filtered.length;
|
||||
|
||||
if (!filtered.length) { grid.innerHTML = ''; empty.style.display = ''; return; }
|
||||
empty.style.display = 'none';
|
||||
|
||||
const catMap = {};
|
||||
CATS.forEach(c => catMap[c.key] = c);
|
||||
|
||||
grid.innerHTML = filtered.map((img, i) => {
|
||||
const c = catMap[img.cat] || {dot:'#888', section:'reference'};
|
||||
const fname = img.path.split('/').pop();
|
||||
return `<div class="card ${c.section}" onclick="openLb(${i})" title="${img.path}">
|
||||
<img src="${img.path}" alt="${fname}">
|
||||
<div class="card-foot">
|
||||
<div class="card-dot" style="background:${c.dot}"></div>
|
||||
<div class="card-label">${fname}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── Navigation ────────────────────────────────────────────────────────────────
|
||||
function selectCat(key) {
|
||||
activeCat = (key === null || key === '') ? null : key;
|
||||
document.querySelectorAll('.nav-item').forEach(el => {
|
||||
const match = activeCat === null ? (el.dataset.cat === '') : (el.dataset.cat === activeCat);
|
||||
el.classList.toggle('active', match);
|
||||
});
|
||||
const c = activeCat ? (CATS.find(c => c.key === activeCat) || {label: activeCat}) : null;
|
||||
document.getElementById('catChip').textContent = c ? c.label : 'All categories';
|
||||
updateCatBar();
|
||||
renderGrid();
|
||||
}
|
||||
|
||||
function setView(size) {
|
||||
viewSize = size;
|
||||
document.getElementById('grid').className = 'grid ' + size;
|
||||
['Large','Medium','Small'].forEach(s =>
|
||||
document.getElementById('v'+s).classList.toggle('active', s.toLowerCase() === size));
|
||||
}
|
||||
|
||||
function onSearch(val) {
|
||||
query = val;
|
||||
document.getElementById('searchClear').style.display = val ? '' : 'none';
|
||||
renderGrid();
|
||||
}
|
||||
function clearSearch() { document.getElementById('search').value=''; onSearch(''); }
|
||||
|
||||
// ── Lightbox ──────────────────────────────────────────────────────────────────
|
||||
function openLb(i) { lbIdx = i; showLb(); document.getElementById('lb').classList.add('open'); }
|
||||
function closeLb() { document.getElementById('lb').classList.remove('open'); }
|
||||
function showLb() {
|
||||
const img = lbFiltered[lbIdx];
|
||||
if (!img) return;
|
||||
document.getElementById('lbImg').src = img.path;
|
||||
document.getElementById('lbFilename').textContent = img.path.split('/').pop();
|
||||
document.getElementById('lbCounter').textContent = `${lbIdx+1} / ${lbFiltered.length}`;
|
||||
const catMap = {}; CATS.forEach(c => catMap[c.key] = c);
|
||||
const c = catMap[img.cat] || {label: img.cat, dot:'#888'};
|
||||
const badge = document.getElementById('lbBadge');
|
||||
badge.textContent = c.label;
|
||||
badge.style.background = c.dot + '22';
|
||||
badge.style.color = c.dot;
|
||||
badge.style.border = '1px solid ' + c.dot + '44';
|
||||
}
|
||||
function lbNav(dir) { lbIdx = (lbIdx + dir + lbFiltered.length) % lbFiltered.length; showLb(); }
|
||||
|
||||
document.getElementById('lb').addEventListener('click', e => { if (e.target.id==='lb') closeLb(); });
|
||||
document.addEventListener('keydown', e => {
|
||||
if (!document.getElementById('lb').classList.contains('open')) return;
|
||||
if (e.key==='ArrowRight'||e.key==='ArrowDown') lbNav(1);
|
||||
if (e.key==='ArrowLeft' ||e.key==='ArrowUp') lbNav(-1);
|
||||
if (e.key==='Escape') closeLb();
|
||||
});
|
||||
|
||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
||||
buildSidebar();
|
||||
updateCatBar();
|
||||
renderGrid();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user