From f61ab7729f7475a0eaaff084d57f85d178338ec3 Mon Sep 17 00:00:00 2001
From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>
Date: Sat, 16 May 2026 20:17:38 -0400
Subject: [PATCH] update
---
src/index.html | 505 +++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 423 insertions(+), 82 deletions(-)
diff --git a/src/index.html b/src/index.html
index ab3295c..7917f82 100644
--- a/src/index.html
+++ b/src/index.html
@@ -1506,6 +1506,7 @@
.feedTrustToggle select{font-size:.75rem;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:4px;padding:3px 6px;cursor:pointer}
body.dark .feedTrustToggle select{background:#0b1220}
body.nostr-logged-in .feedTrustToggle{display:flex}
+ .layerLockTag{display:inline-block;font-size:.58rem;font-weight:800;letter-spacing:.06em;color:var(--muted);background:rgba(127,127,127,.15);border:1px solid var(--line);border-radius:3px;padding:0 4px;margin-right:6px;vertical-align:1px}
.feedTrustBadge{font-size:.62rem;background:rgba(0,184,230,.12);color:var(--accent);border:1px solid rgba(0,184,230,.25);border-radius:4px;padding:1px 5px;margin-left:5px;white-space:nowrap;flex-shrink:0}
body.dark .feedTrustBadge{background:rgba(0,217,255,.1);border-color:rgba(0,217,255,.2)}
.feedCard.feedCardBlurred .feedCardThumb img,
@@ -1540,7 +1541,43 @@
.previewShell { min-height: 420px; }
}
+ body.exporting .slot,
+ body.exporting .discSlot,
+ body.exporting .jewelSlot {
+ border-color: transparent !important;
+ background: transparent !important;
+ }
+ body.exporting .placeholder,
+ body.exporting .slot::after, body.exporting .slot::before,
+ body.exporting .discSlot::before,
+ body.exporting .jewelSlot::before, body.exporting .jewelSlot::after,
+ body.exporting .jewelSpineGuide,
+ body.exporting .foldLine, body.exporting .trimArea, body.exporting .bleedFill,
+ body.exporting .spineText, body.exporting .discSpineStrip {
+ display: none !important;
+ }
+ body.exporting .freeLayer.selected {
+ outline: none !important;
+ }
+ body.exporting .freeLayer {
+ pointer-events: none !important;
+ }
+ body.exporting .sheet,
+ body.exporting .sheet *,
+ body.exporting .designerDiscWrap,
+ body.exporting .designerDiscWrap * {
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ color-adjust: exact !important;
+ }
+
@media print {
+ html, body, .sheet, .sheet *, .freeLayer, .freeLayer * {
+ -webkit-print-color-adjust: exact !important;
+ print-color-adjust: exact !important;
+ color-adjust: exact !important;
+ }
+
html, body {
width: var(--sheet-w) !important;
height: var(--sheet-h) !important;
@@ -1566,16 +1603,21 @@
min-height: 0 !important;
}
+ html:has(body.template-cover) {
+ width: 11in !important;
+ height: 8.5in !important;
+ }
+
body.template-cover {
- width: 8.5in !important;
- height: 11in !important;
+ width: 11in !important;
+ height: 8.5in !important;
}
body.template-cover .app,
body.template-cover main,
body.template-cover .previewShell {
- width: 8.5in !important;
- height: 11in !important;
+ width: 11in !important;
+ height: 8.5in !important;
}
body.template-cover .previewShell {
@@ -1602,8 +1644,8 @@
body.template-cover .sheetWrap {
position: relative !important;
- width: var(--sheet-h) !important;
- height: var(--sheet-w) !important;
+ width: var(--sheet-w) !important;
+ height: var(--sheet-h) !important;
margin: 0 auto !important;
transform: none !important;
}
@@ -1619,13 +1661,13 @@
}
body.template-cover .sheet {
- position: absolute !important;
+ position: fixed !important;
left: 50% !important;
top: 50% !important;
width: var(--sheet-w) !important;
height: var(--sheet-h) !important;
- transform: translate(-50%, -50%) rotate(90deg) !important;
- transform-origin: center center !important;
+ transform: translate(-50%, -50%) !important;
+ margin: 0 !important;
}
.slot, .discSlot, .jewelSlot {
@@ -1845,6 +1887,7 @@
+
@@ -2100,6 +2143,12 @@
+
+
+
+
+
+
@@ -2119,9 +2168,20 @@
+
+
+
+
+
+
+
+
@@ -2159,6 +2219,7 @@
+
@@ -2503,6 +2564,26 @@
const SCHEMA_VERSION = 5;
const MIN_SUPPORTED_SCHEMA_VERSION = 4;
+ const COLOR_LAYER_DEFAULT_FILL = '#3b82f6';
+ const COLOR_LAYER_DEFAULT_OPACITY = 0.6;
+ const WEB_SAFE_FONTS = [
+ { label: 'System default', value: 'inherit' },
+ { label: 'Sans · Arial', value: 'Arial, Helvetica, sans-serif' },
+ { label: 'Sans · Helvetica', value: 'Helvetica, Arial, sans-serif' },
+ { label: 'Sans · Verdana', value: 'Verdana, Geneva, sans-serif' },
+ { label: 'Sans · Trebuchet', value: '"Trebuchet MS", sans-serif' },
+ { label: 'Sans · Tahoma', value: 'Tahoma, Geneva, sans-serif' },
+ { label: 'Sans · Arial Black', value: '"Arial Black", sans-serif' },
+ { label: 'Sans · Impact', value: 'Impact, "Arial Narrow Bold", sans-serif' },
+ { label: 'Serif · Times', value: '"Times New Roman", Times, serif' },
+ { label: 'Serif · Georgia', value: 'Georgia, serif' },
+ { label: 'Serif · Palatino', value: '"Palatino Linotype", "Book Antiqua", Palatino, serif' },
+ { label: 'Mono · Courier', value: '"Courier New", Courier, monospace' },
+ { label: 'Mono · Consolas', value: 'Consolas, "Courier New", monospace' },
+ { label: 'Cursive · Comic Sans', value: '"Comic Sans MS", "Comic Sans", cursive' },
+ { label: 'Cursive · Brush Script', value: '"Brush Script MT", cursive' }
+ ];
+
const presets = {
dvd: {
name: 'DVD case wrap',
@@ -3334,25 +3415,25 @@
function preferredDiscProjectTitle() {
const topTitle = state.discMeta?.top?.title?.trim();
const bottomTitle = state.discMeta?.bottom?.title?.trim();
- return topTitle || bottomTitle || '';
+ return topTitle || bottomTitle || (state.meta?.title || '');
}
function preferredDiscProjectImdb() {
const topImdb = state.discMeta?.top?.imdbId?.trim();
const bottomImdb = state.discMeta?.bottom?.imdbId?.trim();
- return topImdb || bottomImdb || '';
+ return topImdb || bottomImdb || (state.meta?.imdbId || '');
}
function preferredDiscProjectCategory() {
const topCategory = state.discMeta?.top?.category?.trim();
const bottomCategory = state.discMeta?.bottom?.category?.trim();
- return topCategory || bottomCategory || '';
+ return topCategory || bottomCategory || (state.meta?.category || '');
}
function preferredDiscProjectLanguage() {
const topLanguage = state.discMeta?.top?.language?.trim();
const bottomLanguage = state.discMeta?.bottom?.language?.trim();
- return topLanguage || bottomLanguage || 'en';
+ return topLanguage || bottomLanguage || (state.meta?.language || 'en');
}
function syncProjectMetaFromDiscMeta(force = false) {
@@ -3517,8 +3598,8 @@
root.style.setProperty('--bleed', inches(state.bleed));
root.style.setProperty('--safe', inches(state.safe));
- const pageW = state.templateMode === 'cover' ? 8.5 : sheetW();
- const pageH = state.templateMode === 'cover' ? 11 : sheetH();
+ const pageW = state.templateMode === 'cover' ? 11 : sheetW();
+ const pageH = state.templateMode === 'cover' ? 8.5 : sheetH();
$('dynamicPageStyle').textContent = `@page { size: ${fmt(pageW)}in ${fmt(pageH)}in; margin: 0; }`;
if (state.templateMode === 'disc') {
@@ -4477,6 +4558,7 @@
rotate: 0,
opacity: 1,
z: nextLayerZ(),
+ fontFamily: 'inherit',
fontSize: 24,
color: '#111827',
align: 'center',
@@ -4485,6 +4567,27 @@
});
}
+ function addColorLayer(targetOverride = '') {
+ const target = targetOverride || $('layerTarget')?.value || 'page';
+ const bounds = targetBounds(target);
+ applyTargetMode(target);
+ addLayer({
+ id: makeLayerId(),
+ type: 'color',
+ name: 'Color block',
+ target,
+ color: COLOR_LAYER_DEFAULT_FILL,
+ x: bounds.x,
+ y: bounds.y,
+ w: bounds.w,
+ h: bounds.h,
+ rotate: 0,
+ opacity: COLOR_LAYER_DEFAULT_OPACITY,
+ locked: true,
+ z: nextLayerZ()
+ });
+ }
+
function renderLayers() {
const overlay = $('layerOverlay');
const designerOverlay = $('designerLayerOverlay');
@@ -4515,18 +4618,25 @@
el.style.opacity = layer.opacity;
el.style.zIndex = layer.z || 1;
el.style.transform = `rotate(${layer.rotate || 0}deg)`;
+ if (layer.locked) {
+ el.style.pointerEvents = 'none';
+ el.classList.add('layerLocked');
+ }
if (layer.type === 'image') {
const img = document.createElement('img');
img.src = layer.src;
img.alt = layer.name || 'Image layer';
el.appendChild(img);
+ } else if (layer.type === 'color') {
+ el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else {
const text = document.createElement('div');
text.className = 'textLayer';
text.contentEditable = 'true';
text.spellcheck = false;
text.innerHTML = layer.html || '';
+ text.style.fontFamily = layer.fontFamily || 'inherit';
text.style.fontSize = `${layer.fontSize || 24}pt`;
text.style.color = layer.color || '#111827';
text.style.textAlign = layer.align || 'left';
@@ -4576,9 +4686,12 @@
const item = document.createElement('button');
item.type = 'button';
item.className = `layerItem${layer.id === state.selectedLayerId ? ' active' : ''}`;
- const rawName = layer.name || layer.type;
+ const rawName = layer.type === 'text'
+ ? (layerHtmlToPlainText(layer.html).split('\n')[0].trim() || layer.name || 'Text layer')
+ : (layer.name || layer.type);
const displayName = rawName.length > 32 ? rawName.slice(0, 29) + '...' : rawName;
- item.innerHTML = `${displayName}${layer.type}`;
+ const lockTag = layer.locked ? 'LOCK' : '';
+ item.innerHTML = `${lockTag}${displayName}${layer.type}`;
item.addEventListener('click', () => {
state.selectedLayerId = layer.id;
render();
@@ -4635,7 +4748,11 @@
addTextBtn.type = 'button';
addTextBtn.textContent = 'Add text layer';
addTextBtn.addEventListener('click', () => addTextLayer(targetDef.key));
- buttonRow.append(addImageBtn, addTextBtn);
+ const addColorBtn = document.createElement('button');
+ addColorBtn.type = 'button';
+ addColorBtn.textContent = 'Add color block';
+ addColorBtn.addEventListener('click', () => addColorLayer(targetDef.key));
+ buttonRow.append(addImageBtn, addTextBtn, addColorBtn);
actions.append(urlRow, buttonRow);
panel.appendChild(actions);
@@ -4655,9 +4772,12 @@
const item = document.createElement('button');
item.type = 'button';
item.className = `layerItem${layer.id === state.selectedLayerId ? ' active' : ''}`;
- const rawName = layer.name || layer.type;
+ const rawName = layer.type === 'text'
+ ? (layerHtmlToPlainText(layer.html).split('\n')[0].trim() || layer.name || 'Text layer')
+ : (layer.name || layer.type);
const displayName = rawName.length > 32 ? rawName.slice(0, 29) + '...' : rawName;
- item.innerHTML = `${displayName}${layer.type}`;
+ const lockTag = layer.locked ? 'LOCK' : '';
+ item.innerHTML = `${lockTag}${displayName}${layer.type}`;
item.addEventListener('click', () => {
state.selectedLayerId = layer.id;
render();
@@ -5524,20 +5644,48 @@
return getLayer(state.selectedLayerId);
}
+ function layerHtmlToPlainText(html) {
+ if (!html) return '';
+ const tmp = document.createElement('div');
+ tmp.innerHTML = String(html).replace(/
/gi, '\n').replace(/<\/(?:div|p)>/gi, '\n').replace(/<(?:div|p)[^>]*>/gi, '');
+ return (tmp.textContent || '').replace(/\n{3,}/g, '\n\n');
+ }
+ function layerPlainTextToHtml(text) {
+ return escapeHtml(String(text || '')).replace(/\n/g, '
');
+ }
+
function syncLayerInputs() {
const layer = selectedLayer();
$('layerControls').open = Boolean(layer);
$('textLayerControls').style.display = layer && layer.type === 'text' ? 'block' : 'none';
$('layerImageControls').style.display = layer && layer.type === 'image' ? 'block' : 'none';
+ const colorPanel = $('colorLayerControls');
+ if (colorPanel) colorPanel.style.display = layer && layer.type === 'color' ? 'block' : 'none';
+ const layerNameRow = $('layerName');
+ const layerNameLabel = layerNameRow?.previousElementSibling;
+ const hideName = layer && layer.type === 'text';
+ if (layerNameRow) layerNameRow.style.display = hideName ? 'none' : '';
+ if (layerNameLabel && layerNameLabel.tagName === 'LABEL' && layerNameLabel.getAttribute('for') === 'layerName') {
+ layerNameLabel.style.display = hideName ? 'none' : '';
+ }
for (const id of ['layerName', 'layerX', 'layerY', 'layerW', 'layerH', 'layerRotate', 'layerOpacity']) {
$(id).disabled = !layer;
}
$('layerUrl').disabled = !layer || layer.type !== 'image';
- for (const id of ['textFontSize', 'textColor', 'textAlign', 'textBold', 'textItalic']) {
- $(id).disabled = !layer || layer.type !== 'text';
+ for (const id of ['textLayerContent', 'textFontFamily', 'textFontSize', 'textColor', 'textAlign', 'textBold', 'textItalic']) {
+ const el = $(id);
+ if (el) el.disabled = !layer || layer.type !== 'text';
}
- for (const id of ['layerForward', 'layerBackward', 'deleteLayer']) {
- $(id).disabled = !layer;
+ const fillEl = $('colorLayerFill');
+ if (fillEl) fillEl.disabled = !layer || layer.type !== 'color';
+ for (const id of ['layerForward', 'layerBackward', 'layerDuplicate', 'deleteLayer']) {
+ const el = $(id);
+ if (el) el.disabled = !layer;
+ }
+ const lockEl = $('layerLocked');
+ if (lockEl) {
+ lockEl.disabled = !layer;
+ lockEl.checked = !!(layer && layer.locked);
}
if (!layer) return;
@@ -5554,10 +5702,18 @@
}
if (layer.type === 'text') {
+ const content = $('textLayerContent');
+ if (content && document.activeElement !== content) content.value = layerHtmlToPlainText(layer.html);
+ const ff = $('textFontFamily');
+ if (ff) ff.value = layer.fontFamily || 'inherit';
$('textFontSize').value = layer.fontSize || 24;
$('textColor').value = layer.color || '#111827';
$('textAlign').value = layer.align || 'left';
}
+
+ if (layer.type === 'color' && fillEl) {
+ fillEl.value = layer.color || COLOR_LAYER_DEFAULT_FILL;
+ }
}
function updateSelectedLayer(prop, value) {
@@ -5571,6 +5727,7 @@
const el = event.currentTarget;
const layer = getLayer(el.dataset.layerId);
if (!layer) return;
+ if (layer.locked) { state.selectedLayerId = layer.id; render(); return; }
state.selectedLayerId = layer.id;
const textTarget = event.target.closest('.textLayer');
@@ -5792,6 +5949,82 @@
URL.revokeObjectURL(url);
}
+ let _html2canvasPromise = null;
+ function loadHtml2Canvas() {
+ if (window.html2canvas) return Promise.resolve(window.html2canvas);
+ if (_html2canvasPromise) return _html2canvasPromise;
+ _html2canvasPromise = new Promise((resolve, reject) => {
+ const s = document.createElement('script');
+ s.src = 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js';
+ s.onload = () => resolve(window.html2canvas);
+ s.onerror = () => { _html2canvasPromise = null; reject(new Error('Could not load the image-export library. Check your network connection.')); };
+ document.head.appendChild(s);
+ });
+ return _html2canvasPromise;
+ }
+
+ function jpegFilenameStem() {
+ const parts = [
+ filenamePart(state.meta?.title) || 'design',
+ filenamePart(state.meta?.category),
+ state.appMode === 'designer' ? 'disc_design' : state.templateMode
+ ].filter(Boolean);
+ return parts.join('_');
+ }
+
+ async function exportArtboardAsJpeg(button) {
+ const originalLabel = button?.textContent;
+ const originalSelectedId = state.selectedLayerId;
+ const sheetWrapEl = $('sheetWrap');
+ const originalSheetWrapTransform = sheetWrapEl?.style.transform || '';
+ const targetEl = state.appMode === 'designer' ? $('designerDiscWrap') : $('sheet');
+ if (!targetEl) return;
+ try {
+ if (button) { button.disabled = true; button.textContent = 'Rendering…'; }
+ if (originalSelectedId) { state.selectedLayerId = ''; }
+ if (sheetWrapEl) sheetWrapEl.style.transform = 'none';
+ document.body.classList.add('exporting');
+ render();
+ await new Promise(res => requestAnimationFrame(() => requestAnimationFrame(() => setTimeout(res, 100))));
+ await Promise.all([...targetEl.querySelectorAll('img')].map(img => {
+ if (img.complete && img.naturalHeight) return Promise.resolve();
+ return new Promise(res => {
+ img.addEventListener('load', () => res(), { once: true });
+ img.addEventListener('error', () => res(), { once: true });
+ });
+ }));
+ const html2canvas = await loadHtml2Canvas();
+ const rect = targetEl.getBoundingClientRect();
+ const scale = Math.max(2, Math.min(4, 1800 / Math.max(rect.width, rect.height)));
+ const canvas = await html2canvas(targetEl, {
+ scale,
+ useCORS: true,
+ allowTaint: false,
+ backgroundColor: '#ffffff',
+ logging: false,
+ imageTimeout: 10000
+ });
+ const blob = await new Promise(res => canvas.toBlob(res, 'image/jpeg', 0.92));
+ if (!blob) throw new Error('Could not encode JPEG. One of your images is from a server that blocks cross-origin reads — try a different image host.');
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = `${jpegFilenameStem()}.jpg`;
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ } catch (err) {
+ alert(err.message || 'Could not export JPEG.');
+ } finally {
+ document.body.classList.remove('exporting');
+ if (sheetWrapEl) sheetWrapEl.style.transform = originalSheetWrapTransform;
+ if (originalSelectedId) { state.selectedLayerId = originalSelectedId; }
+ render();
+ if (button) { button.disabled = false; button.textContent = originalLabel; }
+ }
+ }
+
function downloadProject() {
const title = (state.meta.title || '').trim();
const category = (state.meta.category || '').trim();
@@ -5823,62 +6056,74 @@
triggerDownload(payload, filename);
}
- function closeDiscDesignerProject() {
- const hasContent = state.discDesign?.src || (state.layers && state.layers.some(l => l.target === 'designer-disc'));
- if (!hasContent) {
- state.discDesign = { name: '', src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 };
- state.layers = state.layers.filter(l => l.target !== 'designer-disc');
- state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
- render();
- return;
- }
- const action = confirm('Save this design before closing?\n\nOK = Save to file\nCancel = Close without saving');
- if (action) {
- try {
- downloadSingleDesign();
- } catch (e) {
- const publishAnyway = confirm(`Could not save: ${e.message}\n\nPublish to Nostr instead?`);
- if (publishAnyway) {
- try {
- publishDiscDesignToNostr();
- } catch (e2) {
- alert(`Error: ${e2.message}`);
- return;
- }
- } else {
- return;
- }
- }
- }
+ function resetDiscDesignerState() {
state.discDesign = { name: '', src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 };
- state.layers = state.layers.filter(l => l.target !== 'designer-disc');
- state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
+ state.layers = (state.layers || []).filter(l => l.target !== 'designer-disc');
+ state.meta = { title: '', imdbId: '', omdbApiKey: '', category: '', language: 'en' };
+ state.selectedLayerId = '';
+ syncInputs();
+ syncDesignerInputs();
render();
}
- function closeTemplateProject() {
- const hasContent = state.discs.top?.src || state.discs.bottom?.src || state.combined?.src || Object.values(state.slots || {}).some(s => s?.src);
- if (!hasContent) {
- state.discs = { top: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, bottom: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
- state.discMeta = normalizeDiscMeta();
- state.slots = { back: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, spine: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, front: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
- state.combined = { src: '' };
- state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
- render();
- return;
- }
- const action = confirm('Save this project before closing?\n\nOK = Save to file\nCancel = Close without saving');
- if (action) {
- downloadProject();
+ function closeDiscDesignerProject() {
+ const hasContent = state.discDesign?.src || (state.layers && state.layers.some(l => l.target === 'designer-disc'));
+ if (hasContent) {
+ const action = confirm('Save this design before closing?\n\nOK = Save to file\nCancel = Close without saving');
+ if (action) {
+ try {
+ downloadSingleDesign();
+ } catch (e) {
+ const publishAnyway = confirm(`Could not save: ${e.message}\n\nPublish to Nostr instead?`);
+ if (publishAnyway) {
+ try {
+ publishDiscDesignToNostr();
+ } catch (e2) {
+ alert(`Error: ${e2.message}`);
+ return;
+ }
+ } else {
+ return;
+ }
+ }
+ }
}
+ resetDiscDesignerState();
+ }
+
+ function resetTemplateProjectState() {
state.discs = { top: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, bottom: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
state.discMeta = normalizeDiscMeta();
state.slots = { back: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, spine: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, front: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
state.combined = { src: '' };
- state.meta = { title: "", imdbId: "", omdbApiKey: "", category: "", language: "en" };
+ state.jewelInserts = { front: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 }, tray: { src: '', fit: 'cover', zoom: 1, x: 0, y: 0, rotate: 0 } };
+ state.discSpineLabels = { top: '', bottom: '' };
+ state.jewelSpineLabels = { left: '', right: '' };
+ state.meta = { title: '', imdbId: '', omdbApiKey: '', category: '', language: 'en' };
+ state.layers = (state.layers || []).filter(l => l.target === 'designer-disc');
+ state.selectedLayerId = '';
+ syncInputs();
+ syncAllSlotInputs();
+ syncAllDiscInputs();
+ syncAllJewelInputs();
+ syncCombinedInputs();
render();
}
+ function closeTemplateProject() {
+ const hasSlotContent = state.discs.top?.src || state.discs.bottom?.src || state.combined?.src || Object.values(state.slots || {}).some(s => s?.src);
+ const hasJewel = Object.values(state.jewelInserts || {}).some(i => i?.src);
+ const hasLayers = (state.layers || []).some(l => l.target !== 'designer-disc' && !l._generatedDiscLayer);
+ const hasSpine = !!(state.discSpineLabels?.top || state.discSpineLabels?.bottom || state.jewelSpineLabels?.left || state.jewelSpineLabels?.right);
+ const hasTitle = !!(state.meta?.title?.trim());
+ const hasContent = hasSlotContent || hasJewel || hasLayers || hasSpine || hasTitle;
+ if (hasContent) {
+ const action = confirm('Save this project before closing?\n\nOK = Save to file\nCancel = Close without saving');
+ if (action) downloadProject();
+ }
+ resetTemplateProjectState();
+ }
+
function loadProjectFromText(text) {
const parsed = JSON.parse(text);
if (parsed.version && parsed.templateMode === 'disc-design' && parsed.discDesign) {
@@ -7205,8 +7450,10 @@
if (layer.type === 'text') {
const fontPx = (layer.fontSize || 14) * (96/72) * SCALE;
- el.style.cssText += `color:${layer.color||'#fff'};font-size:${fontPx}px;font-weight:${layer.bold?'bold':'normal'};font-style:${layer.italic?'italic':'normal'};text-align:${layer.align||'left'};display:flex;align-items:flex-start;`;
+ el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color||'#fff'};font-size:${fontPx}px;font-weight:${layer.bold?'bold':'normal'};font-style:${layer.italic?'italic':'normal'};text-align:${layer.align||'left'};display:flex;align-items:flex-start;`;
el.innerHTML = layer.html || '';
+ } else if (layer.type === 'color') {
+ el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
const img = document.createElement('img');
img.src = layer.src;
@@ -7377,10 +7624,57 @@
preview.appendChild(el);
});
+ const coverLayers = (Array.isArray(payload.layers) ? payload.layers : [])
+ .filter(l => {
+ const t = String(l?.target || '');
+ return !t.startsWith('disc') && !t.startsWith('jewel') && !t.startsWith('designer-') && !l._generatedDiscLayer;
+ })
+ .slice()
+ .sort((a, b) => (a.z || 0) - (b.z || 0));
+
+ const renderCoverLayer = (wrap, layer) => {
+ const el = document.createElement('div');
+ const lx = ((Number(layer.x) || 0) - bleed) / totalW * 100;
+ const ly = ((Number(layer.y) || 0) - bleed) / trimH * 100;
+ const lw = (Number(layer.w) || totalW) / totalW * 100;
+ const lh = (Number(layer.h) || trimH) / trimH * 100;
+ el.style.cssText = [
+ 'position:absolute',
+ `left:${lx}%`,
+ `top:${ly}%`,
+ `width:${lw}%`,
+ `height:${lh}%`,
+ `transform:rotate(${Number(layer.rotate) || 0}deg)`,
+ `opacity:${layer.opacity !== undefined ? layer.opacity : 1}`,
+ 'overflow:hidden',
+ 'pointer-events:none',
+ 'box-sizing:border-box',
+ 'z-index:4'
+ ].join(';');
+ if (layer.type === 'text') {
+ const scale = preview.clientWidth ? (preview.clientWidth / (totalW * 96)) : 1;
+ const align = layer.align || 'left';
+ el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
+ el.innerHTML = layer.html || '';
+ } else if (layer.type === 'color') {
+ el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
+ } else if (layer.type === 'image' && layer.src) {
+ const img = document.createElement('img');
+ img.src = layer.src;
+ img.alt = layer.name || '';
+ img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
+ img.addEventListener('error', () => img.remove());
+ el.appendChild(img);
+ }
+ wrap.appendChild(el);
+ };
+
+ coverLayers.forEach(layer => renderCoverLayer(preview, layer));
+
const row = document.createElement('div');
row.appendChild(preview);
- const hasArtwork = (isCombined && combined.src) || ['back','spine','front'].some(key => slots[key]?.src);
+ const hasArtwork = (isCombined && combined.src) || ['back','spine','front'].some(key => slots[key]?.src) || coverLayers.length > 0;
if (!hasArtwork) {
if (payload.preview) {
const img = document.createElement('img');
@@ -7439,8 +7733,10 @@
if (layer.type === 'text') {
const scale = wrap.clientWidth && layerScale ? (wrap.clientWidth / (layerScale * 96)) : 1;
const align = layer.align || 'left';
- el.style.cssText += `color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
+ el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
el.innerHTML = layer.html || '';
+ } else if (layer.type === 'color') {
+ el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
const img = document.createElement('img');
img.src = layer.src;
@@ -7711,8 +8007,10 @@
if (layer.type === 'text') {
const scale = wrap.clientWidth ? (wrap.clientWidth / (slotW * 96)) : 1;
const align = layer.align || 'left';
- el.style.cssText += `color:${layer.color || '#111'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
+ el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#111'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
el.innerHTML = layer.html || '';
+ } else if (layer.type === 'color') {
+ el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
const img = document.createElement('img');
img.src = layer.src;
@@ -8248,6 +8546,10 @@
$('saveProjectBtn').addEventListener('click', downloadProject);
$('closeProjectBtn').addEventListener('click', closeTemplateProject);
+ const exportJpegBtn = $('exportJpegBtn');
+ if (exportJpegBtn) exportJpegBtn.addEventListener('click', () => exportArtboardAsJpeg(exportJpegBtn));
+ const designerExportJpegBtn = $('designerExportJpegBtn');
+ if (designerExportJpegBtn) designerExportJpegBtn.addEventListener('click', () => exportArtboardAsJpeg(designerExportJpegBtn));
$('designerSaveProjectBtn').addEventListener('click', () => {
try {
downloadSingleDesign();
@@ -8569,9 +8871,27 @@
$(id).addEventListener('input', (event) => updateSelectedLayer(prop, Number(event.target.value)));
}
+ const textContentEl = $('textLayerContent');
+ if (textContentEl) {
+ textContentEl.addEventListener('input', (event) => {
+ const layer = selectedLayer();
+ if (!layer || layer.type !== 'text') return;
+ layer.html = layerPlainTextToHtml(event.target.value);
+ render();
+ });
+ }
+ const fontFamilyEl = $('textFontFamily');
+ if (fontFamilyEl) {
+ fontFamilyEl.innerHTML = WEB_SAFE_FONTS.map(f => ``).join('');
+ fontFamilyEl.addEventListener('change', (event) => updateSelectedLayer('fontFamily', event.target.value));
+ }
$('textFontSize').addEventListener('input', (event) => updateSelectedLayer('fontSize', Number(event.target.value)));
$('textColor').addEventListener('input', (event) => updateSelectedLayer('color', event.target.value));
$('textAlign').addEventListener('change', (event) => updateSelectedLayer('align', event.target.value));
+ const colorFillEl = $('colorLayerFill');
+ if (colorFillEl) colorFillEl.addEventListener('input', (event) => updateSelectedLayer('color', event.target.value));
+ const lockEl = $('layerLocked');
+ if (lockEl) lockEl.addEventListener('change', (event) => updateSelectedLayer('locked', event.target.checked));
$('textBold').addEventListener('click', () => {
const layer = selectedLayer();
@@ -8587,20 +8907,25 @@
render();
});
- $('layerForward').addEventListener('click', () => {
+ function moveLayerStep(direction) {
const layer = selectedLayer();
if (!layer) return;
- layer.z = nextLayerZ();
+ const curZ = layer.z || 0;
+ const peers = state.layers
+ .filter(l => l !== layer && l.target === layer.target && !l._generatedDiscLayer)
+ .sort((a, b) => (a.z || 0) - (b.z || 0));
+ const swap = direction === 'forward'
+ ? peers.find(l => (l.z || 0) > curZ)
+ : [...peers].reverse().find(l => (l.z || 0) < curZ);
+ if (!swap) return;
+ const swapZ = swap.z || 0;
+ swap.z = curZ;
+ layer.z = swapZ;
render();
- });
+ }
- $('layerBackward').addEventListener('click', () => {
- const layer = selectedLayer();
- if (!layer) return;
- const minZ = state.layers.reduce((min, item) => Math.min(min, item.z || 0), layer.z || 0);
- layer.z = minZ - 1;
- render();
- });
+ $('layerForward').addEventListener('click', () => moveLayerStep('forward'));
+ $('layerBackward').addEventListener('click', () => moveLayerStep('backward'));
$('deleteLayer').addEventListener('click', () => {
if (!state.selectedLayerId) return;
@@ -8609,6 +8934,22 @@
render();
});
+ const duplicateBtn = $('layerDuplicate');
+ if (duplicateBtn) duplicateBtn.addEventListener('click', () => {
+ const layer = selectedLayer();
+ if (!layer) return;
+ const OFFSET = 0.1;
+ const copy = {
+ ...layer,
+ id: makeLayerId(),
+ x: (Number(layer.x) || 0) + OFFSET,
+ y: (Number(layer.y) || 0) + OFFSET,
+ z: nextLayerZ(),
+ name: layer.name ? `${layer.name} copy` : layer.name
+ };
+ addLayer(copy);
+ });
+
window.addEventListener('keydown', (event) => {
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'p') {
render();