PageSourceSearch

https://jayreddinjr-jr-odysseus-hf.hf.space/static/js/editor/wire-import.js

js jayreddinjr-jr-odysseus-hf.hf.space collected 2026-10-04 17:01:24 UTC 7,001 bytes, 148 lines download raw bytes

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.