diff --git a/newtab.html b/newtab.html
index d234357..628c3c7 100644
--- a/newtab.html
+++ b/newtab.html
@@ -73,6 +73,9 @@
+
@@ -197,6 +200,16 @@
+
+
+ Bild-Referenz Widgets
+ Bilder als Referenz anzeigen (nur aktuelle Session)
+
+
+
@@ -483,6 +496,7 @@
+
diff --git a/src/css/main.css b/src/css/main.css
index 53f4d2c..0ca3164 100644
--- a/src/css/main.css
+++ b/src/css/main.css
@@ -1364,6 +1364,91 @@ body.show-desc .bm-desc { display: block; }
color: var(--bg-primary);
}
+/* ============================================
+ IMAGE REFERENCE WIDGET
+ ============================================ */
+.imgref-container {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ padding: 8px;
+}
+.imgref-img-wrapper {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ border-radius: var(--radius-sm);
+ min-height: 80px;
+}
+.imgref-img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ border-radius: var(--radius-sm);
+}
+.imgref-dropzone {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ border: 2px dashed var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-muted);
+ font-size: 12px;
+ cursor: pointer;
+ transition: border-color 0.2s, color 0.2s;
+ min-height: 80px;
+ gap: 8px;
+}
+.imgref-dropzone:hover,
+.imgref-dropzone.dragover {
+ border-color: var(--accent);
+ color: var(--text-secondary);
+}
+.imgref-dropzone-icon {
+ font-size: 24px;
+ opacity: 0.5;
+}
+.imgref-label {
+ width: 100%;
+ margin-top: 8px;
+ font-size: 11px;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-secondary);
+ padding: 4px 8px;
+ flex-shrink: 0;
+}
+.imgref-label:focus {
+ border-color: var(--accent);
+ outline: none;
+}
+.imgref-label::placeholder {
+ color: var(--text-muted);
+}
+.imgref-replace-btn {
+ width: 100%;
+ height: 24px;
+ margin-top: 6px;
+ font-size: 10px;
+ background: transparent;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--text-muted);
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: all 0.2s;
+}
+.imgref-replace-btn:hover {
+ border-color: var(--accent);
+ color: var(--text-secondary);
+}
+
/* ============================================
WIDGET TOOLBAR
============================================ */
diff --git a/src/js/app.js b/src/js/app.js
index 6316f82..5bf4352 100644
--- a/src/js/app.js
+++ b/src/js/app.js
@@ -20,6 +20,7 @@ async function init() {
await Notes.init();
await Calculator.init();
await Timer.init();
+ await ImageRef.init();
initDataButtons();
Store.checkQuota();
diff --git a/src/js/image-ref.js b/src/js/image-ref.js
new file mode 100644
index 0000000..2a1f230
--- /dev/null
+++ b/src/js/image-ref.js
@@ -0,0 +1,500 @@
+/* =============================================
+ HELLION NEWTAB — image-ref.js
+ Bild-Referenz Widget: Session-only Bildanzeige
+ mit Canvas API WebP-Konvertierung
+ ============================================= */
+
+const ImageRef = {
+ MAX_IMAGES: 3,
+ STORAGE_KEY: 'widgetStates',
+ SESSION_KEY: 'imageRefData',
+
+ /** @type {Array<{id: string, label: string, x: number, y: number, width: number, height: number, open: boolean}>} */
+ _images: [],
+ _saveTimer: null,
+
+ // ---- STORAGE (persistent: Position/Meta) ----
+
+ /**
+ * Widget-Meta aus persistentem Storage laden
+ */
+ async load() {
+ const data = await Store.get(this.STORAGE_KEY);
+ if (data && data.imageRef && Array.isArray(data.imageRef.images)) {
+ this._images = data.imageRef.images;
+ }
+ },
+
+ /**
+ * Widget-Meta persistent speichern
+ * Bestehende Notes, Calculator, Timer bleiben erhalten
+ */
+ async save() {
+ const data = await Store.get(this.STORAGE_KEY) || {};
+ if (data.notes === undefined) data.notes = [];
+
+ // Positionen aus WidgetManager aktualisieren
+ const updated = this._images.map(img => {
+ const ws = WidgetManager.getState(img.id);
+ if (ws) {
+ img.x = ws.x;
+ img.y = ws.y;
+ img.width = ws.width;
+ img.height = ws.height;
+ img.open = ws.open;
+ }
+ return img;
+ });
+
+ data.imageRef = { images: updated };
+ await Store.set(this.STORAGE_KEY, data);
+ },
+
+ /**
+ * Debounced Save
+ */
+ _debouncedSave() {
+ clearTimeout(this._saveTimer);
+ this._saveTimer = setTimeout(() => this.save(), 500);
+ },
+
+ // ---- SESSION STORAGE (Bilddaten) ----
+
+ /**
+ * Bilddaten in sessionStorage speichern
+ */
+ _saveSession() {
+ try {
+ const sessionData = {};
+ this._images.forEach(img => {
+ const dataUrl = this._getSessionImage(img.id);
+ if (dataUrl) sessionData[img.id] = dataUrl;
+ });
+ sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(sessionData));
+ } catch (e) {
+ console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e);
+ }
+ },
+
+ /**
+ * Bilddaten aus sessionStorage laden
+ * @returns {Object} - { id: dataUrl, ... }
+ */
+ _loadSessionAll() {
+ try {
+ const raw = sessionStorage.getItem(this.SESSION_KEY);
+ if (!raw) return {};
+ return JSON.parse(raw);
+ } catch (e) {
+ console.warn('ImageRef: sessionStorage Read fehlgeschlagen', e);
+ return {};
+ }
+ },
+
+ /**
+ * Einzelnes Bild aus sessionStorage lesen
+ * @param {string} id
+ * @returns {string|null}
+ */
+ _getSessionImage(id) {
+ const all = this._loadSessionAll();
+ return all[id] || null;
+ },
+
+ /**
+ * Einzelnes Bild in sessionStorage setzen
+ * @param {string} id
+ * @param {string} dataUrl
+ */
+ _setSessionImage(id, dataUrl) {
+ try {
+ const all = this._loadSessionAll();
+ all[id] = dataUrl;
+ sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(all));
+ } catch (e) {
+ console.warn('ImageRef: sessionStorage Write fehlgeschlagen', e);
+ HellionDialog.alert(
+ 'Bild konnte nicht gespeichert werden. Der Browser-Speicher ist voll.',
+ { type: 'danger', title: 'Speicherfehler' }
+ );
+ }
+ },
+
+ /**
+ * Einzelnes Bild aus sessionStorage entfernen
+ * @param {string} id
+ */
+ _removeSessionImage(id) {
+ try {
+ const all = this._loadSessionAll();
+ delete all[id];
+ sessionStorage.setItem(this.SESSION_KEY, JSON.stringify(all));
+ } catch (e) {
+ console.warn('ImageRef: sessionStorage Remove fehlgeschlagen', e);
+ }
+ },
+
+ // ---- WIDGET LIFECYCLE ----
+
+ /**
+ * Neues Bild-Widget erstellen (oeffnet File-Dialog)
+ */
+ async create() {
+ if (!settings.imageRefEnabled) return;
+
+ if (this._images.length >= this.MAX_IMAGES) {
+ await HellionDialog.alert(
+ 'Maximal ' + this.MAX_IMAGES + ' Bild-Widgets gleichzeitig. Schliesse eines um ein neues zu oeffnen.',
+ { type: 'warning', title: 'Limit erreicht' }
+ );
+ return;
+ }
+
+ // Freie ID finden
+ const usedIds = new Set(this._images.map(i => i.id));
+ let slotId = null;
+ for (let i = 0; i < this.MAX_IMAGES; i++) {
+ const candidate = 'image_' + i;
+ if (!usedIds.has(candidate)) {
+ slotId = candidate;
+ break;
+ }
+ }
+ if (!slotId) return;
+
+ // File-Dialog
+ const file = await this._pickFile();
+ if (!file) return;
+
+ // Bild verarbeiten
+ let dataUrl;
+ try {
+ dataUrl = await this._processFile(file);
+ } catch (err) {
+ await HellionDialog.alert(
+ 'Bild konnte nicht geladen werden: ' + err.message,
+ { type: 'danger', title: 'Bildfehler' }
+ );
+ return;
+ }
+
+ // In sessionStorage speichern
+ this._setSessionImage(slotId, dataUrl);
+
+ // Meta erstellen
+ const imageData = {
+ id: slotId,
+ label: '',
+ x: 200 + (this._images.length * 40),
+ y: 120 + (this._images.length * 30),
+ width: 320,
+ height: 280,
+ open: true
+ };
+ this._images.push(imageData);
+
+ // Widget erstellen
+ this._createWidget(imageData, dataUrl);
+ await this.save();
+ },
+
+ /**
+ * Widget im DOM erstellen
+ * @param {Object} imageData
+ * @param {string|null} dataUrl
+ */
+ _createWidget(imageData, dataUrl) {
+ WidgetManager.create('image', {
+ id: imageData.id,
+ title: imageData.label || 'Bild-Referenz',
+ x: imageData.x,
+ y: imageData.y,
+ width: imageData.width,
+ height: imageData.height,
+ open: imageData.open !== false
+ });
+
+ const body = WidgetManager.getBody(imageData.id);
+ if (body) this.renderBody(imageData, body, dataUrl);
+ },
+
+ /**
+ * Widget geschlossen — Daten aufraeumen
+ * @param {string} id
+ */
+ async onClose(id) {
+ this._removeSessionImage(id);
+ this._images = this._images.filter(img => img.id !== id);
+ await this.save();
+ },
+
+ // ---- UI RENDERING ----
+
+ /**
+ * Widget-Body rendern
+ * @param {Object} imageData
+ * @param {HTMLElement} bodyEl
+ * @param {string|null} dataUrl
+ */
+ renderBody(imageData, bodyEl, dataUrl) {
+ bodyEl.textContent = '';
+ const container = document.createElement('div');
+ container.className = 'imgref-container';
+
+ if (dataUrl) {
+ // Bild anzeigen
+ const wrapper = document.createElement('div');
+ wrapper.className = 'imgref-img-wrapper';
+
+ const img = document.createElement('img');
+ img.className = 'imgref-img';
+ img.src = dataUrl;
+ img.alt = imageData.label || 'Bild-Referenz';
+ wrapper.appendChild(img);
+
+ // Bild ersetzen Button
+ const replaceBtn = document.createElement('button');
+ replaceBtn.className = 'imgref-replace-btn';
+ replaceBtn.type = 'button';
+ replaceBtn.textContent = 'Bild ersetzen';
+ replaceBtn.addEventListener('click', async () => {
+ const file = await this._pickFile();
+ if (!file) return;
+ try {
+ const newDataUrl = await this._processFile(file);
+ this._setSessionImage(imageData.id, newDataUrl);
+ this.renderBody(imageData, bodyEl, newDataUrl);
+ } catch (err) {
+ await HellionDialog.alert(
+ 'Bild konnte nicht geladen werden: ' + err.message,
+ { type: 'danger', title: 'Bildfehler' }
+ );
+ }
+ });
+
+ container.append(wrapper, replaceBtn);
+ } else {
+ // Drop-Zone (kein Bild vorhanden)
+ const dropzone = this._createDropzone(imageData, bodyEl);
+ container.appendChild(dropzone);
+ }
+
+ // Label-Input
+ const label = document.createElement('input');
+ label.className = 'imgref-label';
+ label.type = 'text';
+ label.placeholder = 'Beschriftung (optional)';
+ label.maxLength = 100;
+ label.value = imageData.label || '';
+
+ label.addEventListener('input', () => {
+ const text = label.value.trim().slice(0, 100);
+ imageData.label = text;
+
+ // Widget-Titel aktualisieren
+ const entry = WidgetManager._widgets.get(imageData.id);
+ if (entry) {
+ const titleEl = entry.el.querySelector('.widget-title-text');
+ if (titleEl) titleEl.textContent = text || 'Bild-Referenz';
+ entry.state.title = text || 'Bild-Referenz';
+ }
+
+ this._debouncedSave();
+ });
+
+ container.appendChild(label);
+ bodyEl.appendChild(container);
+ },
+
+ /**
+ * Drop-Zone erstellen (fuer leere Widgets / neue Bilder)
+ * @param {Object} imageData
+ * @param {HTMLElement} bodyEl
+ * @returns {HTMLElement}
+ */
+ _createDropzone(imageData, bodyEl) {
+ const dropzone = document.createElement('div');
+ dropzone.className = 'imgref-dropzone';
+
+ const icon = document.createElement('div');
+ icon.className = 'imgref-dropzone-icon';
+ icon.textContent = '\uD83D\uDDBC\uFE0F';
+
+ const text = document.createElement('span');
+ text.textContent = 'Klicken oder Bild hierher ziehen';
+
+ dropzone.append(icon, text);
+
+ // Klick -> File-Dialog
+ dropzone.addEventListener('click', async () => {
+ const file = await this._pickFile();
+ if (!file) return;
+ try {
+ const dataUrl = await this._processFile(file);
+ this._setSessionImage(imageData.id, dataUrl);
+ this.renderBody(imageData, bodyEl, dataUrl);
+ await this.save();
+ } catch (err) {
+ await HellionDialog.alert(
+ 'Bild konnte nicht geladen werden: ' + err.message,
+ { type: 'danger', title: 'Bildfehler' }
+ );
+ }
+ });
+
+ // Drag & Drop
+ dropzone.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dropzone.classList.add('dragover');
+ });
+
+ dropzone.addEventListener('dragleave', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dropzone.classList.remove('dragover');
+ });
+
+ dropzone.addEventListener('drop', async (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ dropzone.classList.remove('dragover');
+
+ const file = e.dataTransfer.files[0];
+ if (!file || !file.type.startsWith('image/')) {
+ await HellionDialog.alert(
+ 'Bitte eine Bilddatei verwenden (PNG, JPG, WebP, etc.).',
+ { type: 'warning', title: 'Kein Bild' }
+ );
+ return;
+ }
+
+ try {
+ const dataUrl = await this._processFile(file);
+ this._setSessionImage(imageData.id, dataUrl);
+ this.renderBody(imageData, bodyEl, dataUrl);
+ await this.save();
+ } catch (err) {
+ await HellionDialog.alert(
+ 'Bild konnte nicht geladen werden: ' + err.message,
+ { type: 'danger', title: 'Bildfehler' }
+ );
+ }
+ });
+
+ return dropzone;
+ },
+
+ // ---- FILE HANDLING ----
+
+ /**
+ * File-Dialog oeffnen
+ * @returns {Promise}
+ */
+ _pickFile() {
+ return new Promise(resolve => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = 'image/*';
+ input.addEventListener('change', () => {
+ resolve(input.files[0] || null);
+ });
+ // Cancel erkennen
+ input.addEventListener('cancel', () => resolve(null));
+ input.click();
+ });
+ },
+
+ /**
+ * Bild per Canvas API zu WebP konvertieren
+ * @param {File} file
+ * @returns {Promise} WebP DataURL
+ */
+ _processFile(file) {
+ return new Promise((resolve, reject) => {
+ const objectUrl = URL.createObjectURL(file);
+ const img = new Image();
+
+ img.onload = () => {
+ try {
+ const canvas = document.createElement('canvas');
+ canvas.width = img.width;
+ canvas.height = img.height;
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(img, 0, 0);
+ const webpUrl = canvas.toDataURL('image/webp', 0.85);
+ URL.revokeObjectURL(objectUrl);
+ resolve(webpUrl);
+ } catch (err) {
+ URL.revokeObjectURL(objectUrl);
+ reject(err);
+ }
+ };
+
+ img.onerror = () => {
+ URL.revokeObjectURL(objectUrl);
+ reject(new Error('Bild konnte nicht geladen werden'));
+ };
+
+ img.src = objectUrl;
+ });
+ },
+
+ // ---- INIT ----
+
+ /**
+ * ImageRef initialisieren (aus app.js aufgerufen)
+ */
+ async init() {
+ await this.load();
+
+ // Widgets wiederherstellen (nur wenn Feature aktiviert)
+ if (settings.imageRefEnabled && this._images.length > 0) {
+ const sessionData = this._loadSessionAll();
+
+ this._images.forEach(imageData => {
+ if (imageData.open !== false) {
+ const dataUrl = sessionData[imageData.id] || null;
+ this._createWidget(imageData, dataUrl);
+ }
+ });
+ }
+
+ // Close-Event abfangen
+ const self = this;
+ const prevClose = WidgetManager.close;
+ WidgetManager.close = function(id) {
+ prevClose.call(WidgetManager, id);
+ // Pruefen ob es ein Image-Widget ist
+ const isImage = self._images.some(img => img.id === id);
+ if (isImage) {
+ self.onClose(id);
+ }
+ };
+
+ // Minimize-Event abfangen
+ const prevMinimize = WidgetManager.minimize;
+ WidgetManager.minimize = async function(id) {
+ await prevMinimize.call(WidgetManager, id);
+ const isImage = self._images.some(img => img.id === id);
+ if (isImage) {
+ await self.save();
+ }
+ };
+
+ // Open-Event abfangen
+ const prevOpen = WidgetManager.openWidget;
+ WidgetManager.openWidget = async function(id) {
+ await prevOpen.call(WidgetManager, id);
+ const imgData = self._images.find(img => img.id === id);
+ if (imgData) {
+ const body = WidgetManager.getBody(id);
+ if (body && body.children.length === 0) {
+ const dataUrl = self._getSessionImage(id);
+ self.renderBody(imgData, body, dataUrl);
+ }
+ await self.save();
+ }
+ };
+ }
+};
diff --git a/src/js/notes.js b/src/js/notes.js
index 5f44caf..d0b7f13 100644
--- a/src/js/notes.js
+++ b/src/js/notes.js
@@ -53,6 +53,9 @@ const Notes = {
if (existing && existing.timer) {
saveData.timer = existing.timer;
}
+ if (existing && existing.imageRef) {
+ saveData.imageRef = existing.imageRef;
+ }
await Store.set(this.STORAGE_KEY, saveData);
},
@@ -527,6 +530,8 @@ const Notes = {
Calculator.toggle();
} else if (action === 'timer') {
Timer.toggle();
+ } else if (action === 'image-ref') {
+ ImageRef.create();
} else if (action === 'notebook') {
this.openNotebook();
}
diff --git a/src/js/settings.js b/src/js/settings.js
index 7f9bc95..f8d12ca 100644
--- a/src/js/settings.js
+++ b/src/js/settings.js
@@ -71,6 +71,13 @@ function applySettings() {
const showSearchEl = document.getElementById('settingShowSearch');
if (showSearchEl) showSearchEl.checked = settings.showSearch;
+ // Image-Ref Toggle
+ if (settings.imageRefEnabled === undefined) settings.imageRefEnabled = false;
+ const imgRefCheckbox = document.getElementById('settingImageRef');
+ if (imgRefCheckbox) imgRefCheckbox.checked = settings.imageRefEnabled;
+ const imgRefBtn = document.querySelector('[data-action="image-ref"]');
+ if (imgRefBtn) imgRefBtn.classList.toggle('hidden', !settings.imageRefEnabled);
+
// Toolbar-Position
document.body.classList.toggle('toolbar-left', settings.toolbarPos === 'left');
const toolbarPosEl = document.getElementById('settingToolbarPos');
@@ -127,6 +134,11 @@ function bindSettingsEvents() {
settingShowSearch: v => {
settings.showSearch = v;
document.getElementById('searchBarWrapper').classList.toggle('hidden', !v);
+ },
+ settingImageRef: v => {
+ settings.imageRefEnabled = v;
+ const imgBtn = document.querySelector('[data-action="image-ref"]');
+ if (imgBtn) imgBtn.classList.toggle('hidden', !v);
}
};
@@ -204,7 +216,8 @@ function bindSettingsEvents() {
boards = [];
settings = { compact: false, shortenTitles: false, newTab: true, showDesc: false,
hideExtra: false, visibleCount: 10, bgUrl: '', theme: 'nebula',
- showSearch: true, searchEngine: 'google', toolbarPos: 'right' };
+ showSearch: true, searchEngine: 'google', toolbarPos: 'right',
+ imageRefEnabled: false };
await saveBoards();
await saveSettings();
applySettings();
diff --git a/src/js/state.js b/src/js/state.js
index 8ceff72..9696616 100644
--- a/src/js/state.js
+++ b/src/js/state.js
@@ -16,7 +16,8 @@ let settings = {
theme: 'nebula',
showSearch: true,
searchEngine: 'google',
- toolbarPos: 'right'
+ toolbarPos: 'right',
+ imageRefEnabled: false
};
function uid() {