Merge pull request #1 from PR0M3TH3AN/codex/implement-multiple-feature-additions-and-updates

Add project metadata, dark/glitch mode, Nostr integration, and structured JSON export
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-05-14 10:49:55 -04:00
commit 21cd78c2dd
1 file changed
+165 -6
+165 -6
View File
@@ -6,10 +6,22 @@
<title>DVD / Blu-ray Cover Template Maker</title>
<style id="dynamicPageStyle">
@page { size: 11in 8.5in; margin: 0; }
body.dark {
--bg:#07070a; --panel:#10131a; --ink:#e5e7eb; --muted:#9aa4b2; --line:#2c3443; --accent:#60a5fa;
background-image: repeating-linear-gradient(90deg, rgba(255,0,255,.06) 0 2px, transparent 2px 6px), repeating-linear-gradient(0deg, rgba(0,255,255,.05) 0 3px, transparent 3px 8px);
}
body.dark aside, body.dark fieldset, body.dark details, body.dark .pill, body.dark .previewShell { background: var(--panel); color: var(--ink); border-color: var(--line); }
body.dark .previewShell { background:#0f1622; box-shadow: inset 0 1px 0 rgba(255,255,255,.03); }
body.dark input[type="number"], body.dark input[type="text"], body.dark select, body.dark button, body.dark input[type="file"] { background:#0b1220; color:var(--ink); border-color:var(--line); }
body.dark .hint, body.dark label, body.dark .subhead, body.dark .status { color: var(--muted); }
body.dark .sheet{filter:contrast(1.08) saturate(1.15) brightness(.95);}
body.dark .layoutTab.active, body.dark button.primary { color:#fff; }
.sheetWrap{margin:0 auto;}
</style>
<style>
:root {
--bg: #f4f5f7;
--noise: none;
--panel: #ffffff;
--ink: #111827;
--muted: #6b7280;
@@ -671,6 +683,15 @@
color: #475569;
}
.modal { position: fixed; inset: 0; background: rgba(0,0,0,.6); display:none; z-index:1000; }
.modal.open { display:block; }
.modalCard { width:min(900px,92vw); max-height:88vh; overflow:auto; margin:4vh auto; background:var(--panel); color:var(--ink); border:1px solid var(--line); border-radius:16px; padding:14px; }
.searchGrid{display:grid;grid-template-columns:1fr 140px 110px;gap:8px;align-items:end}
.results{display:grid;gap:8px;margin-top:10px}
.resultItem{display:grid;grid-template-columns:100px 1fr auto;gap:10px;align-items:center;border:1px solid var(--line);border-radius:10px;padding:8px;background:rgba(127,127,127,.07)}
.resultItem img{width:100px;height:70px;object-fit:cover;border-radius:6px;border:1px solid var(--line)}
@media (max-width: 900px) {
.app { grid-template-columns: 1fr; }
aside { position: relative; max-height: none; border-right: 0; border-bottom: 1px solid var(--line); }
@@ -773,6 +794,17 @@
display: none !important;
}
}
body.dark {
--bg:#07070a; --panel:#10131a; --ink:#e5e7eb; --muted:#9aa4b2; --line:#2c3443; --accent:#60a5fa;
background-image: repeating-linear-gradient(90deg, rgba(255,0,255,.06) 0 2px, transparent 2px 6px), repeating-linear-gradient(0deg, rgba(0,255,255,.05) 0 3px, transparent 3px 8px);
}
body.dark aside, body.dark fieldset, body.dark details, body.dark .pill, body.dark .previewShell { background: var(--panel); color: var(--ink); border-color: var(--line); }
body.dark .previewShell { background:#0f1622; box-shadow: inset 0 1px 0 rgba(255,255,255,.03); }
body.dark input[type="number"], body.dark input[type="text"], body.dark select, body.dark button, body.dark input[type="file"] { background:#0b1220; color:var(--ink); border-color:var(--line); }
body.dark .hint, body.dark label, body.dark .subhead, body.dark .status { color: var(--muted); }
body.dark .sheet{filter:contrast(1.08) saturate(1.15) brightness(.95);}
body.dark .layoutTab.active, body.dark button.primary { color:#fff; }
.sheetWrap{margin:0 auto;}
</style>
</head>
<body>
@@ -829,6 +861,20 @@
<div class="miniReadout" id="sizeReadout"></div>
</fieldset>
<fieldset>
<legend>Project metadata</legend>
<label for="titleField">Title</label><input id="titleField" type="text" placeholder="Movie title" />
<label for="imdbField">IMDb ID</label><input id="imdbField" type="text" placeholder="tt1234567" />
<div class="buttonRow"><button type="button" id="imdbImportBtn">Import IMDb data</button><button type="button" id="nostrExportBtn">Export to Nostr</button><button type="button" id="nostrImportBtn">Import from Nostr</button><button type="button" id="nostrSearchBtn">Search Nostr</button></div>
<p class="hint">IMDb import uses OMDb-compatible endpoint + API key.</p>
</fieldset>
<fieldset>
<legend>Appearance</legend>
<label class="toggle"><input id="darkMode" type="checkbox" /> Enable dark / glitch theme</label>
<p class="hint">Affects editor panels, controls, and preview shell.</p>
</fieldset>
<fieldset>
<legend>View / export</legend>
<label for="zoom">Preview zoom</label>
@@ -843,10 +889,10 @@
<label class="toggle"><input id="showCrops" type="checkbox" checked /> Show crop marks on screen</label>
<label class="toggle"><input id="printGuides" type="checkbox" /> Include guides in PDF</label>
<label class="toggle"><input id="printCrops" type="checkbox" checked /> Include crop marks in PDF</label>
</div>
</div>
<div class="buttonRow">
<button class="primary" id="printBtn">Export PDF</button>
<button class="primary" id="printBtn">Print / Save PDF</button>
<button id="saveProjectBtn">Save project</button>
<button id="loadProjectBtn">Load project</button>
<input id="projectFile" type="file" accept="application/json,.json" hidden />
@@ -1102,6 +1148,20 @@
</main>
</div>
<div class="modal" id="nostrModal" aria-hidden="true">
<div class="modalCard">
<h2 style="margin:0 0 8px">Nostr template search</h2>
<div class="searchGrid">
<div><label for="nostrQuery">Title / IMDb</label><input id="nostrQuery" type="text" placeholder="e.g. The Matrix or tt0133093" /></div>
<div><label for="nostrType">Template</label><select id="nostrType"><option value="any">Any</option><option value="cover">Cover</option><option value="disc">Disc</option></select></div>
<div class="buttonRow" style="margin:0"><button type="button" id="nostrRunSearch">Search</button><button type="button" id="nostrClose">Close</button></div>
</div>
<div class="results" id="nostrResults"></div>
<p class="hint">Search queries recent templates from relay.damus.io (kind 30078).</p>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
@@ -1148,6 +1208,8 @@
};
const state = {
meta: { title: "", imdbId: "", omdbApiKey: "" },
darkMode: false,
templateMode: 'cover',
preset: 'dvd',
panelW: presets.dvd.panelW,
@@ -1243,6 +1305,9 @@
$('zoomOut').textContent = `${state.zoom}%`;
$('artworkMode').value = state.artworkMode;
$('showGuides').checked = state.showGuides;
const dm=$('darkMode'); if (dm) dm.checked = !!state.darkMode;
$('titleField').value = state.meta?.title || "";
$('imdbField').value = state.meta?.imdbId || "";
$('showBleed').checked = state.showBleed;
$('showCrops').checked = state.showCrops;
$('printGuides').checked = state.printGuides;
@@ -1257,6 +1322,7 @@
document.body.classList.toggle('hide-crops', !state.showCrops);
document.body.classList.toggle('print-guides', state.printGuides);
document.body.classList.toggle('print-crops', state.printCrops);
document.body.classList.toggle('dark', !!state.darkMode);
}
function renderDimensions() {
@@ -1918,8 +1984,19 @@
render();
}
function exportPayload() {
return {
version: 2,
meta: state.meta,
ui: { zoom: state.zoom, darkMode: state.darkMode, showGuides: state.showGuides, showBleed: state.showBleed, showCrops: state.showCrops, printGuides: state.printGuides, printCrops: state.printCrops },
coverTemplate: { preset: state.preset, panelW: state.panelW, trimH: state.trimH, spineW: state.spineW, bleed: state.bleed, safe: state.safe, artworkMode: state.artworkMode, slots: state.slots, combined: state.combined },
discTemplate: { config: discTemplate, discs: state.discs },
layers: state.layers
};
}
function downloadProject() {
const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' });
const blob = new Blob([JSON.stringify(exportPayload(), null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
const date = new Date().toISOString().slice(0, 10);
@@ -1933,7 +2010,8 @@
function loadProjectFromText(text) {
const parsed = JSON.parse(text);
const next = { ...state, ...parsed };
const source = parsed.version ? { ...parsed.ui, ...parsed.coverTemplate, discs: parsed.discTemplate?.discs, meta: parsed.meta, layers: parsed.layers } : parsed;
const next = { ...state, ...source };
next.slots = { ...state.slots, ...(parsed.slots || {}) };
next.combined = { ...state.combined, ...(parsed.combined || {}) };
next.discs = { ...state.discs, ...(parsed.discs || {}) };
@@ -1947,6 +2025,56 @@
render();
}
function previewFromPayload(payload, typeHint) {
try {
if (typeHint === 'disc' || payload.templateMode === 'disc') {
return payload.discs?.top?.src || payload.discTemplate?.discs?.top?.src || '';
}
return payload.slots?.front?.src || payload.coverTemplate?.slots?.front?.src || '';
} catch { return ''; }
}
async function runNostrSearch() {
const q = $('nostrQuery').value.trim().toLowerCase();
const type = $('nostrType').value;
const out = $('nostrResults');
out.innerHTML = '<div class="hint">Searching…</div>';
const ws = new WebSocket('wss://relay.damus.io');
const subId = `s-${Date.now()}`;
const events = [];
await new Promise((resolve) => {
const timeout = setTimeout(() => { try { ws.close(); } catch {} resolve(); }, 3500);
ws.onopen = () => ws.send(JSON.stringify(['REQ', subId, { kinds:[30078], limit:120 }]));
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data[0] === 'EVENT' && data[1] === subId) events.push(data[2]);
if (data[0] === 'EOSE') { clearTimeout(timeout); ws.close(); resolve(); }
};
ws.onerror = () => { clearTimeout(timeout); resolve(); };
});
const rows = [];
for (const e of events) {
let payload = null;
try { payload = JSON.parse(e.content || '{}'); } catch { continue; }
const title = payload.meta?.title || '';
const imdb = payload.meta?.imdbId || '';
const mode = payload.templateMode || (payload.coverTemplate ? 'cover' : 'disc');
const hay = `${title} ${imdb} ${(e.tags||[]).map(t=>t.join(' ')).join(' ')}`.toLowerCase();
if (q && !hay.includes(q)) continue;
if (type !== 'any' && mode !== type) continue;
rows.push({e,payload,title,imdb,mode,preview:previewFromPayload(payload, mode)});
}
out.innerHTML = rows.length ? '' : '<div class="hint">No templates found.</div>';
rows.slice(0,40).forEach((row) => {
const el = document.createElement('div');
el.className = 'resultItem';
el.innerHTML = `<img src="${row.preview || ''}" alt="preview" /><div><b>${row.title || 'Untitled'}</b><div class="hint">${row.imdb || 'No IMDb'} • ${row.mode}</div></div><button type="button">Import</button>`;
el.querySelector('button').addEventListener('click', ()=> loadProjectFromText(JSON.stringify(row.payload)));
out.appendChild(el);
});
}
function attachEvents() {
for (const mode of ['cover', 'disc']) {
$(`mode-${mode}`).addEventListener('click', () => {
@@ -1971,13 +2099,44 @@
render();
});
for (const id of ['showGuides', 'showBleed', 'showCrops', 'printGuides', 'printCrops']) {
for (const id of ['showGuides', 'showBleed', 'showCrops', 'printGuides', 'printCrops','darkMode']) {
$(id).addEventListener('change', (event) => {
state[id] = event.target.checked;
if (id === "darkMode") state.darkMode = event.target.checked; else state[id] = event.target.checked;
render();
});
}
$("titleField").addEventListener("input", (e)=>{state.meta.title=e.target.value; render();});
$("imdbField").addEventListener("input", (e)=>{state.meta.imdbId=e.target.value; render();});
$("imdbImportBtn").addEventListener("click", async ()=>{
const imdb = state.meta.imdbId.trim();
const key = prompt("OMDb API key:", state.meta.omdbApiKey || "");
if (!imdb || !key) return;
state.meta.omdbApiKey = key;
const res = await fetch(`https://www.omdbapi.com/?i=${encodeURIComponent(imdb)}&apikey=${encodeURIComponent(key)}`);
const data = await res.json();
if (data && data.Title) { state.meta.title = data.Title; $("titleField").value = data.Title; render(); } else alert("IMDb import failed");
});
$("nostrExportBtn").addEventListener("click", async ()=>{
if (!window.nostr) return alert("No Nostr extension found");
const evt={kind:30078,created_at:Math.floor(Date.now()/1000),tags:[["d", state.meta.imdbId||state.meta.title||"template"]],content:JSON.stringify(state)};
evt.pubkey = await window.nostr.getPublicKey();
const signed = await window.nostr.signEvent(evt);
await navigator.clipboard.writeText(JSON.stringify(signed));
alert("Signed Nostr event copied to clipboard");
});
$("nostrSearchBtn").addEventListener("click", ()=>{ $("nostrModal").classList.add("open"); });
$("nostrClose").addEventListener("click", ()=>{ $("nostrModal").classList.remove("open"); });
$("nostrRunSearch").addEventListener("click", runNostrSearch);
$("nostrImportBtn").addEventListener("click", ()=>{
const raw = prompt("Paste Nostr event JSON"); if (!raw) return;
try { const evt = JSON.parse(raw); if (evt.content) loadProjectFromText(evt.content); } catch { alert("Bad event JSON"); }
});
$('printBtn').addEventListener('click', () => {
render();
window.print();