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 @@ Print & Save
+
@@ -2100,6 +2143,12 @@
+ + + + + + @@ -2119,9 +2168,20 @@
+ + + +
+
@@ -2159,6 +2219,7 @@ Save & Export
+
@@ -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();