1/** 2 * Image-import wiring â covers all four entry points that drop an 3 * image as a new layer: 4 * 5 * #ge-import-topbar topbar "+ Import" button 6 * #ge-import-file File button in the Import section 7 * #ge-import-paste Clipboard button (uses async clipboard API) 8 * #ge-import-gallery Gallery picker â fetches /api/gallery/library 9 * and shows a thumbnail grid overlay 10 * 11 * Plus the shared `handleImportedImage(img)` sink â scales to canvas, 12 * centres, creates a new layer, switches to Move tool, hides the 13 * import section, refreshes the panel. Returned so the drag-and-drop 14 * + paste paths (wired in editor/clipboard-and-drop.js) can use the 15 * same sink. 16 * 17 * @param {{ 18 * container: HTMLElement, 19 * saveState: (label?: string) => void, 20 * createLayer: (name, w, h) => object, 21 * composite: () => void, 22 * renderLayerPanel: () => void, 23 * uiModule: object, 24 * }} deps 25 * 26 * @returns {{ handleImportedImage: (img: HTMLImageElement) => void }} 27 */ 28import { state } from './state.js'; 29 30export function wireImport({ container, saveState, createLayer, composite, renderLayerPanel, uiModule }) { 31 // Hidden <input type="file"> the topbar + File buttons both click. 32 const importFileInput = document.createElement('input'); 33 importFileInput.type = 'file'; 34 importFileInput.accept = 'image/*'; 35 importFileInput.style.display = 'none'; 36 container.appendChild(importFileInput); 37 38 function handleImportedImage(img) { 39 if (!state.editorOpen) return; 40 saveState('Import image'); 41 // Scale down if larger than canvas. 42 let w = img.naturalWidth || img.width; 43 let h = img.naturalHeight || img.height; 44 if (w > state.imgWidth || h > state.imgHeight) { 45 const scale = Math.min(state.imgWidth / w, state.imgHeight / h); 46 w = Math.round(w * scale); 47 h = Math.round(h * scale); 48 } 49 const layer = createLayer('Imported', state.imgWidth, state.imgHeight); 50 // Centre on the canvas. 51 const ox = Math.round((state.imgWidth - w) / 2); 52 const oy = Math.round((state.imgHeight - h) / 2); 53 layer.ctx.drawImage(img, ox, oy, w, h); 54 state.layers.push(layer); 55 state.activeLayerId = layer.id; 56 // Switch to move tool so the imported layer is immediately 57 // repositionable. 58 state.tool = 'move'; 59 const tb = container.querySelector('.ge-toolbar'); 60 if (tb) tb.querySelectorAll('.ge-tool-btn').forEach(b => b.classList.toggle('active', b.dataset.tool === 'move')); 61 // Hide the import section now that the import is done. 62 const importSec = document.getElementById('ge-import-section'); 63 if (importSec) importSec.style.display = 'none'; 64 composite(); 65 renderLayerPanel(); 66 if (uiModule) uiModule.showToast('Image imported â drag to position'); 67 } 68 69 importFileInput.addEventListener('change', (e) => { 70 const file = e.target.files[0]; 71 if (!file) return; 72 const reader = new FileReader(); 73 reader.onload = (ev) => { 74 const img = new Image(); 75 img.onload = () => handleImportedImage(img); 76 img.src = ev.target.result; 77 }; 78 reader.readAsDataURL(file); 79 importFileInput.value = ''; 80 }); 81 82 document.getElementById('ge-import-topbar')?.addEventListener('click', () => importFileInput.click()); 83 document.getElementById('ge-import-file')?.addEventListener('click', () => importFileInput.click()); 84 85 document.getElementById('ge-import-paste')?.addEventListener('click', async () => { 86 try { 87 const clipItems = await navigator.clipboard.read(); 88 let blob = null; 89 for (const item of clipItems) { 90 const imgType = item.types.find(t => t.startsWith('image/')); 91 if (imgType) { blob = await item.getType(imgType); break; } 92 } 93 if (!blob) { if (uiModule) uiModule.showToast('No image found in clipboard'); return; } 94 const url = URL.createObjectURL(blob); 95 const img = new Image(); 96 img.onload = () => { handleImportedImage(img); URL.revokeObjectURL(url); }; 97 img.onerror = () => { URL.revokeObjectURL(url); if (uiModule) uiModule.showToast('Failed to load clipboard image'); }; 98 img.src = url; 99 } catch (e) { 100 if (uiModule) uiModule.showToast('Clipboard access denied or no image available'); 101 } 102 }); 103 104 // Import from Gallery â fetch /api/gallery/library and show a 105 // thumbnail-grid picker overlay. 106 document.getElementById('ge-import-gallery')?.addEventListener('click', async () => { 107 try { 108 const res = await fetch('/api/gallery/library?limit=50', { credentials: 'same-origin' }); 109 const data = await res.json(); 110 const items = data.items || []; 111 if (!items.length) { if (uiModule) uiModule.showToast('No images in gallery'); return; } 112 113 // Picker overlay.
114 const overlay = document.createElement('div'); 115 overlay.style.cssText = 'position:fixed;inset:0;z-index:10001;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;'; 116 const panel = document.createElement('div'); 117 panel.style.cssText = 'background:var(--panel,#1e1e1e);border-radius:12px;padding:16px;max-width:500px;max-height:70vh;overflow-y:auto;width:90%;'; 118 panel.innerHTML = '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"><span style="font-size:13px;font-weight:600;">Pick from Gallery</span><button id="ge-gallery-close" style="background:none;border:none;color:var(--fg);cursor:pointer;font-size:18px;">â</button></div>'; 119 const grid = document.createElement('div'); 120 grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(80px,1fr));gap:6px;'; 121 for (const item of items) { 122 const thumb = document.createElement('img'); 123 thumb.src = item.url; 124 thumb.style.cssText = 'width:100%;aspect-ratio:1;object-fit:cover;border-radius:6px;cursor:pointer;border:2px solid transparent;transition:border-color 0.15s;'; 125 thumb.addEventListener('mouseenter', () => { thumb.style.borderColor = 'var(--accent,#61afef)'; }); 126 thumb.addEventListener('mouseleave', () => { thumb.style.borderColor = 'transparent'; }); 127 thumb.addEventListener('click', () => { 128 overlay.remove(); 129 const img = new Image(); 130 img.crossOrigin = 'anonymous'; 131 img.onload = () => handleImportedImage(img); 132 img.onerror = () => { if (uiModule) uiModule.showToast('Failed to load gallery image'); }; 133 img.src = item.url; 134 }); 135 grid.appendChild(thumb); 136 } 137 panel.appendChild(grid); 138 overlay.appendChild(panel); 139 document.body.appendChild(overlay); 140 overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); }); 141 panel.querySelector('#ge-gallery-close').addEventListener('click', () => overlay.remove()); 142 } catch (e) { 143 if (uiModule) uiModule.showToast('Failed to load gallery: ' + e.message); 144 } 145 }); 146 147 return { handleImportedImage }; 148}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.