backup
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Lahr Clip Browser</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { background: #0a0a0b; color: #eee; font-family: Inter, sans-serif; padding: 24px; }
|
||||
h1 { font-size: 20px; color: #e8291b; margin-bottom: 6px; }
|
||||
p.sub { color: #888; font-size: 13px; margin-bottom: 24px; }
|
||||
.layout { display: grid; grid-template-columns: 1fr 340px; gap: 24px; }
|
||||
#clip-list { display: flex; flex-direction: column; gap: 10px; }
|
||||
.clip-item {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
background: #161618; border: 1px solid #2a2a2e; border-radius: 8px;
|
||||
padding: 10px 12px; cursor: grab; user-select: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.clip-item:hover { border-color: #e8291b; }
|
||||
.clip-item.dragging { opacity: 0.4; }
|
||||
.clip-item.drag-over { border-color: #e8291b; background: #1f1012; }
|
||||
.drag-handle { color: #555; font-size: 18px; flex-shrink: 0; cursor: grab; }
|
||||
.clip-thumb { width: 120px; height: 68px; object-fit: cover; border-radius: 4px; flex-shrink: 0; background: #222; }
|
||||
.clip-info { flex: 1; min-width: 0; }
|
||||
.clip-name { font-size: 13px; font-weight: 600; color: #ddd; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.clip-size { font-size: 11px; color: #666; margin-top: 2px; }
|
||||
.clip-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
|
||||
.btn-preview { background: #1e1e22; border: 1px solid #333; color: #ccc; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
||||
.btn-preview:hover { border-color: #e8291b; color: #e8291b; }
|
||||
.btn-remove { background: transparent; border: 1px solid #333; color: #555; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; }
|
||||
.btn-remove:hover { border-color: #e8291b; color: #e8291b; }
|
||||
|
||||
.sidebar { position: sticky; top: 24px; }
|
||||
.preview-box { background: #161618; border: 1px solid #2a2a2e; border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
||||
.preview-box h3 { font-size: 13px; color: #888; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
#preview-video { width: 100%; border-radius: 6px; background: #000; }
|
||||
#preview-name { font-size: 12px; color: #666; margin-top: 8px; text-align: center; }
|
||||
|
||||
.reel-box { background: #161618; border: 1px solid #2a2a2e; border-radius: 8px; padding: 16px; }
|
||||
.reel-box h3 { font-size: 13px; color: #888; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||
#reel-order { font-size: 11px; color: #666; line-height: 1.8; margin-bottom: 14px; max-height: 220px; overflow-y: auto; }
|
||||
.btn-build { width: 100%; background: #e8291b; color: #fff; border: none; padding: 12px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; }
|
||||
.btn-build:hover { background: #c72216; }
|
||||
#build-output { margin-top: 12px; font-size: 11px; color: #888; background: #0d0d0f; border-radius: 4px; padding: 10px; display: none; white-space: pre-wrap; }
|
||||
|
||||
.unused-section { margin-top: 32px; }
|
||||
.unused-section h2 { font-size: 14px; color: #555; margin-bottom: 12px; }
|
||||
#unused-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.unused-item { display: flex; align-items: center; gap: 12px; background: #111; border: 1px solid #1e1e22; border-radius: 8px; padding: 8px 12px; }
|
||||
.unused-item .clip-name { font-size: 12px; color: #666; }
|
||||
.btn-add { background: #1e1e22; border: 1px solid #333; color: #888; padding: 5px 10px; border-radius: 4px; font-size: 11px; cursor: pointer; margin-left: auto; }
|
||||
.btn-add:hover { border-color: #4a9; color: #4a9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Lahr Clip Browser</h1>
|
||||
<p class="sub">Drag clips to reorder. Click Preview to watch. Remove clips from reel. Build Reel when ready.</p>
|
||||
|
||||
<div class="layout">
|
||||
<div>
|
||||
<div id="clip-list"></div>
|
||||
<div class="unused-section">
|
||||
<h2>Available (not in reel)</h2>
|
||||
<div id="unused-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sidebar">
|
||||
<div class="preview-box">
|
||||
<h3>Preview</h3>
|
||||
<video id="preview-video" controls></video>
|
||||
<div id="preview-name">Click Preview on any clip</div>
|
||||
</div>
|
||||
<div class="reel-box">
|
||||
<h3>Current Reel Order</h3>
|
||||
<div id="reel-order"></div>
|
||||
<button class="btn-build" onclick="buildReel()">Build Reel</button>
|
||||
<div id="build-output"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const BASE = '/assets/videos/hero/clips/';
|
||||
|
||||
const ALL_CLIPS = [
|
||||
{ name: 'v3-shot-01', label: 'v3 · Family enters door, pans to carpet', size: '5.2MB' },
|
||||
{ name: 'v3-shot-02', label: 'v3 · Wine spill on sofa close-up', size: '6.0MB' },
|
||||
{ name: 'v3-shot-03', label: 'v3 · Dirty stained carpet close-up', size: '3.3MB' },
|
||||
{ name: 'v3-shot-04', label: 'v3 · Clean bright sofa pullback', size: '5.6MB' },
|
||||
{ name: 'v3-shot-05', label: 'v3 · Office lobby carpet pan', size: '4.7MB' },
|
||||
{ name: 'v3-shot-06', label: 'v3 · Living room clean carpet pan', size: '4.9MB' },
|
||||
{ name: 'v3-shot-07', label: 'v3 · Restaurant carpet glide', size: '3.1MB' },
|
||||
{ name: 'v2-shot-01-door-entry', label: 'v2 · Door entry muddy boots', size: '2.2MB' },
|
||||
{ name: 'v2-shot-02-mud-on-carpet', label: 'v2 · Mud boots on carpet floor level', size: '4.1MB' },
|
||||
{ name: 'v2-shot-03-stain-on-chair', label: 'v2 · Stain on chair close-up', size: '3.3MB' },
|
||||
{ name: 'v2-shot-04-extraction-carpet', label: 'v2 · Extraction machine on carpet', size: '4.2MB' },
|
||||
{ name: 'v2-shot-05-clean-stairs', label: 'v2 · Clean bright staircase', size: '5.6MB' },
|
||||
{ name: 'v2-shot-06-office', label: 'v2 · Bright office carpet', size: '4.7MB' },
|
||||
{ name: 'v2-shot-07-restaurant', label: 'v2 · Restaurant carpet', size: '4.7MB' },
|
||||
{ name: 'shot-01-door-opens-trimmed', label: 'v1 · Door opens (trimmed 2.5s)', size: '525KB' },
|
||||
{ name: 'shot-01-wide-room', label: 'v1 · Wide room establishing', size: '2.6MB' },
|
||||
{ name: 'shot-02-pan-to-stains', label: 'v1 · Pan to stains / muddy shoes', size: '2.8MB' },
|
||||
{ name: 'shot-02-staircase', label: 'v1 · Staircase', size: '1.4MB' },
|
||||
{ name: 'shot-03-stain-closeup', label: 'v1 · Stain close-up', size: '4.7MB' },
|
||||
{ name: 'shot-03-technician', label: 'v1 · Technician', size: '1.6MB' },
|
||||
{ name: 'shot-04-extraction-carpet', label: 'v1 · Extraction carpet (clean reveal)', size: '6.2MB' },
|
||||
{ name: 'shot-04-extraction-closeup', label: 'v1 · Extraction close-up', size: '2.0MB' },
|
||||
{ name: 'shot-05-clean-reveal', label: 'v1 · Clean reveal', size: '2.2MB' },
|
||||
{ name: 'shot-05-extraction-couch', label: 'v1 · Extraction couch', size: '3.5MB' },
|
||||
{ name: 'shot-06-extraction-stairs', label: 'v1 · Extraction stairs', size: '5.4MB' },
|
||||
{ name: 'shot-07-office-entryway', label: 'v1 · Office entryway', size: '5.9MB' },
|
||||
{ name: 'shot-08-showroom', label: 'v1 · Showroom', size: '4.1MB' },
|
||||
{ name: 'shot-09-technician-unloading', label: 'v1 · Technician unloading van', size: '3.7MB' },
|
||||
{ name: 'shot-01-door-opens', label: 'v1 · Door opens (full 6s)', size: '1.5MB' },
|
||||
];
|
||||
|
||||
// Default reel = current v3 set
|
||||
let reelClips = ALL_CLIPS.slice(0, 7).map(c => c.name);
|
||||
|
||||
function getClip(name) { return ALL_CLIPS.find(c => c.name === name); }
|
||||
|
||||
function render() {
|
||||
const list = document.getElementById('clip-list');
|
||||
list.innerHTML = '';
|
||||
reelClips.forEach((name, idx) => {
|
||||
const clip = getClip(name);
|
||||
if (!clip) return;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'clip-item';
|
||||
div.draggable = true;
|
||||
div.dataset.name = name;
|
||||
div.innerHTML = `
|
||||
<span class="drag-handle">⠿</span>
|
||||
<video class="clip-thumb" src="${BASE}${name}.mp4" muted preload="metadata"></video>
|
||||
<div class="clip-info">
|
||||
<div class="clip-name">${idx+1}. ${clip.label}</div>
|
||||
<div class="clip-size">${clip.size}</div>
|
||||
</div>
|
||||
<div class="clip-actions">
|
||||
<button class="btn-preview" onclick="preview('${name}', '${clip.label}')">Preview</button>
|
||||
<button class="btn-remove" onclick="remove('${name}')">Remove</button>
|
||||
</div>`;
|
||||
div.addEventListener('dragstart', dragStart);
|
||||
div.addEventListener('dragover', dragOver);
|
||||
div.addEventListener('drop', drop);
|
||||
div.addEventListener('dragend', dragEnd);
|
||||
list.appendChild(div);
|
||||
});
|
||||
|
||||
const unused = ALL_CLIPS.filter(c => !reelClips.includes(c.name));
|
||||
const ulist = document.getElementById('unused-list');
|
||||
ulist.innerHTML = '';
|
||||
unused.forEach(clip => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'unused-item';
|
||||
div.innerHTML = `
|
||||
<div class="clip-name">${clip.label} <span style="color:#444">(${clip.size})</span></div>
|
||||
<button class="btn-add" onclick="addToReel('${clip.name}')">+ Add</button>`;
|
||||
ulist.appendChild(div);
|
||||
});
|
||||
|
||||
const orderEl = document.getElementById('reel-order');
|
||||
orderEl.innerHTML = reelClips.map((n,i) => {
|
||||
const c = getClip(n);
|
||||
return `<div>${i+1}. ${c ? c.label : n}</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function preview(name, label) {
|
||||
const v = document.getElementById('preview-video');
|
||||
v.src = BASE + name + '.mp4';
|
||||
v.play();
|
||||
document.getElementById('preview-name').textContent = label;
|
||||
}
|
||||
|
||||
function remove(name) {
|
||||
reelClips = reelClips.filter(n => n !== name);
|
||||
render();
|
||||
}
|
||||
|
||||
function addToReel(name) {
|
||||
reelClips.push(name);
|
||||
render();
|
||||
}
|
||||
|
||||
let dragSrc = null;
|
||||
function dragStart(e) { dragSrc = this; this.classList.add('dragging'); }
|
||||
function dragOver(e) { e.preventDefault(); document.querySelectorAll('.clip-item').forEach(el => el.classList.remove('drag-over')); this.classList.add('drag-over'); }
|
||||
function drop(e) {
|
||||
e.preventDefault();
|
||||
if (dragSrc === this) return;
|
||||
const fromName = dragSrc.dataset.name;
|
||||
const toName = this.dataset.name;
|
||||
const fi = reelClips.indexOf(fromName);
|
||||
const ti = reelClips.indexOf(toName);
|
||||
reelClips.splice(fi, 1);
|
||||
reelClips.splice(ti, 0, fromName);
|
||||
render();
|
||||
}
|
||||
function dragEnd() { document.querySelectorAll('.clip-item').forEach(el => el.classList.remove('dragging','drag-over')); }
|
||||
|
||||
async function buildReel() {
|
||||
const out = document.getElementById('build-output');
|
||||
out.style.display = 'block';
|
||||
out.textContent = 'Sending to server...';
|
||||
|
||||
const resp = await fetch('/tools/build-reel-api.py', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ clips: reelClips })
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
// Fallback: show the ffmpeg command to copy
|
||||
const lines = reelClips.map(n => `file 'assets/videos/hero/clips/${n}.mp4'`).join('\n');
|
||||
out.textContent = 'Server not available. Run this manually:\n\n' +
|
||||
'# 1. Save as concat.txt:\n' + lines + '\n\n' +
|
||||
'# 2. Run ffmpeg:\n' +
|
||||
'ffmpeg -y -f concat -safe 0 -i concat.txt -c:v libx264 -crf 22 -preset fast -movflags +faststart assets/videos/hero/hero-reel.mp4';
|
||||
} else {
|
||||
const data = await resp.json();
|
||||
out.textContent = data.message || 'Done.';
|
||||
}
|
||||
}
|
||||
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user