Files
gott/wwwroot/js/avatar-editor.js
Meleki ce50633e33 refactor: Inline-Styles durch CSS-Klassen ersetzt
- 40+ Utility-Klassen in form.css (clickable, text-danger, flex-row, loading-state, etc.)
- share-menu position/z-index in chat.css statt inline
- index.html: 0 Inline-Styles (hidden-Attribut, CSS-Klassen)
- 12 JS-Dateien bereinigt: style.cssText/style.* durch classList ersetzt
- Bugfix: var(--danger) → var(--dangerous) (nicht existierende Variable)
- display:none Toggles → hidden-Attribut wo passend
- Hardcoded font-sizes in chat.css → var(--font-size)

15 Dateien, nur Frontend (CSS/JS/HTML), kein Backend
2026-03-14 17:14:30 +01:00

336 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ── Avatar Editor Modal ──
// Wiederverwendbar für Session + Agent Avatar-Bearbeitung.
//
// Usage:
// const editor = AvatarEditor.init({
// previewEl: document.getElementById('myPreview'),
// initialImage: 'data:image/...' || null,
// initialScale: 1.5,
// initialOffsetX: 0,
// initialOffsetY: 0,
// onChange(imageData, scale, offsetX, offsetY) { ... }
// });
// // Cleanup:
// editor.destroy();
// // Get current values:
// editor.getImage(); // data-url or null
// editor.getScale(); // number
// editor.getOffsetX(); // number (-50 to 50)
// editor.getOffsetY(); // number (-50 to 50)
var AvatarEditor = (function () {
function init(opts) {
var previewEl = opts.previewEl;
var imageData = opts.initialImage || null;
var scale = opts.initialScale || 1;
var offsetX = opts.initialOffsetX || 0;
var offsetY = opts.initialOffsetY || 0;
var onChange = opts.onChange || function () {};
var overlay = null;
var pasteHandler = null;
var kreaAvailable = false;
// Krea-Status prüfen
apiKreaStatus().then(function (s) { kreaAvailable = s.configured; }).catch(function () {});
function buildTransformStyle() {
var parts = [];
if (scale !== 1) parts.push('scale(' + scale + ')');
if (offsetX !== 0 || offsetY !== 0) parts.push('translate(' + offsetX + '%, ' + offsetY + '%)');
return parts.length ? ' style="transform: ' + parts.join(' ') + '"' : '';
}
// Preview rendern
function renderPreview() {
if (imageData) {
previewEl.innerHTML = '<img src="' + imageData + '" alt=""' + buildTransformStyle() + '>';
previewEl.classList.add('has-image');
} else {
previewEl.innerHTML = '?';
previewEl.classList.remove('has-image');
}
}
renderPreview();
// Click → Modal öffnen
previewEl.classList.add('clickable');
previewEl.addEventListener('click', openModal);
// Paste Handler (global, immer aktiv)
document.addEventListener('paste', pasteHandler = function (e) {
if (overlay) return; // Modal offen → Paste dort behandeln
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image/') === 0) {
e.preventDefault();
readFile(items[i].getAsFile());
return;
}
}
});
function readFile(file) {
if (!file) return;
var reader = new FileReader();
reader.onload = function () {
imageData = reader.result;
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
if (overlay) refreshModal();
};
reader.readAsDataURL(file);
}
function setImage(data) {
imageData = data;
scale = 1;
offsetX = 0;
offsetY = 0;
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
}
function openModal() {
if (overlay) return;
overlay = document.createElement('div');
overlay.className = 'av-overlay';
overlay.innerHTML =
'<div class="av-modal">' +
'<div class="av-header">' +
'<h3>Avatar bearbeiten</h3>' +
'<button class="icon-btn av-close"><i class="fa fa-close"></i></button>' +
'</div>' +
'<div class="av-body">' +
'<div class="av-preview-large">' +
(imageData ? '<img src="' + imageData + '" alt=""' + buildTransformStyle() + '>' : '?') +
'</div>' +
'<div class="av-controls">' +
'<div class="av-scale-row"' + (imageData ? '' : ' hidden') + '>' +
'<label>Zoom</label>' +
'<input type="range" class="av-scale" min="0.5" max="3" step="0.1" value="' + scale + '">' +
'<span class="av-scale-value">' + scale + '×</span>' +
'</div>' +
'<div class="av-offset-row"' + (imageData ? '' : ' hidden') + '>' +
'<label>X</label>' +
'<input type="range" class="av-offset-x" min="-50" max="50" step="1" value="' + offsetX + '">' +
'<span class="av-offset-x-value">' + offsetX + '%</span>' +
'</div>' +
'<div class="av-offset-row"' + (imageData ? '' : ' hidden') + '>' +
'<label>Y</label>' +
'<input type="range" class="av-offset-y" min="-50" max="50" step="1" value="' + offsetY + '">' +
'<span class="av-offset-y-value">' + offsetY + '%</span>' +
'</div>' +
'<div class="av-actions">' +
'<button class="av-upload-btn"><i class="fa fa-upload"></i> Hochladen</button>' +
'<input type="file" class="av-file-input" accept="image/*" hidden>' +
'<button class="av-clear-btn danger-btn"' + (imageData ? '' : ' hidden') + '><i class="fa fa-trash"></i> Entfernen</button>' +
'</div>' +
'<div class="av-ai-section"' + (kreaAvailable ? '' : ' hidden') + '>' +
'<div class="input-with-btn">' +
'<input type="text" class="av-ai-prompt" placeholder="Bildbeschreibung eingeben...">' +
'<button type="button" class="icon-btn av-ai-btn" title="Bild generieren"><i class="fa fa-magic"></i></button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(overlay);
// Refs
var modal = overlay.querySelector('.av-modal');
var closeBtn = overlay.querySelector('.av-close');
var previewLarge = overlay.querySelector('.av-preview-large');
var scaleRow = overlay.querySelector('.av-scale-row');
var scaleInput = overlay.querySelector('.av-scale');
var scaleValueEl = overlay.querySelector('.av-scale-value');
var offsetRows = overlay.querySelectorAll('.av-offset-row');
var offsetXInput = overlay.querySelector('.av-offset-x');
var offsetXValueEl = overlay.querySelector('.av-offset-x-value');
var offsetYInput = overlay.querySelector('.av-offset-y');
var offsetYValueEl = overlay.querySelector('.av-offset-y-value');
var uploadBtn = overlay.querySelector('.av-upload-btn');
var fileInput = overlay.querySelector('.av-file-input');
var clearBtn = overlay.querySelector('.av-clear-btn');
var aiPrompt = overlay.querySelector('.av-ai-prompt');
var aiBtn = overlay.querySelector('.av-ai-btn');
// Close
closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
function updatePreviewLargeTransform() {
var img = previewLarge.querySelector('img');
if (img) {
var parts = [];
if (scale !== 1) parts.push('scale(' + scale + ')');
if (offsetX !== 0 || offsetY !== 0) parts.push('translate(' + offsetX + '%, ' + offsetY + '%)');
img.style.transform = parts.length ? parts.join(' ') : '';
}
}
// Scale
scaleInput.addEventListener('input', function () {
scale = parseFloat(scaleInput.value);
scaleValueEl.textContent = scale + '×';
updatePreviewLargeTransform();
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
});
// Offset X
offsetXInput.addEventListener('input', function () {
offsetX = parseFloat(offsetXInput.value);
offsetXValueEl.textContent = offsetX + '%';
updatePreviewLargeTransform();
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
});
// Offset Y
offsetYInput.addEventListener('input', function () {
offsetY = parseFloat(offsetYInput.value);
offsetYValueEl.textContent = offsetY + '%';
updatePreviewLargeTransform();
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
});
// Upload
uploadBtn.addEventListener('click', function () { fileInput.click(); });
fileInput.addEventListener('change', function () {
if (fileInput.files[0]) readFile(fileInput.files[0]);
});
// Clear
clearBtn.addEventListener('click', function () {
imageData = null;
scale = 1;
offsetX = 0;
offsetY = 0;
scaleInput.value = 1;
scaleValueEl.textContent = '1×';
offsetXInput.value = 0;
offsetXValueEl.textContent = '0%';
offsetYInput.value = 0;
offsetYValueEl.textContent = '0%';
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
refreshModal();
});
// AI Generate
aiBtn.addEventListener('click', generateAi);
aiPrompt.addEventListener('keydown', function (e) {
if (e.key === 'Enter') { e.preventDefault(); generateAi(); }
});
async function generateAi() {
var prompt = aiPrompt.value.trim();
if (!prompt) return;
aiBtn.disabled = true;
aiBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i>';
try {
var result = await apiKreaGenerate(prompt);
if (result.files && result.files.length > 0) {
var imgResp = await fetch('/api/images/file/' + result.files[0]);
var blob = await imgResp.blob();
var reader = new FileReader();
reader.onload = function () {
imageData = reader.result;
scale = 1;
offsetX = 0;
offsetY = 0;
scaleInput.value = 1;
scaleValueEl.textContent = '1×';
offsetXInput.value = 0;
offsetXValueEl.textContent = '0%';
offsetYInput.value = 0;
offsetYValueEl.textContent = '0%';
renderPreview();
onChange(imageData, scale, offsetX, offsetY);
refreshModal();
};
reader.readAsDataURL(blob);
}
} catch (e) {
showToast('Fehler: ' + e.message, 'error');
} finally {
aiBtn.disabled = false;
aiBtn.innerHTML = '<i class="fa fa-magic"></i>';
}
}
// Paste im Modal
modal.addEventListener('paste', function (e) {
var items = e.clipboardData && e.clipboardData.items;
if (!items) return;
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image/') === 0) {
e.preventDefault();
readFile(items[i].getAsFile());
return;
}
}
});
// Escape
overlay._keyHandler = function (e) {
if (e.key === 'Escape') closeModal();
};
document.addEventListener('keydown', overlay._keyHandler);
}
function refreshModal() {
if (!overlay) return;
var previewLarge = overlay.querySelector('.av-preview-large');
var scaleRow = overlay.querySelector('.av-scale-row');
var offsetRows = overlay.querySelectorAll('.av-offset-row');
var clearBtn = overlay.querySelector('.av-clear-btn');
if (imageData) {
previewLarge.innerHTML = '<img src="' + imageData + '" alt=""' + buildTransformStyle() + '>';
scaleRow.hidden = false;
offsetRows.forEach(function (r) { r.hidden = false; });
clearBtn.hidden = false;
} else {
previewLarge.innerHTML = '?';
scaleRow.hidden = true;
offsetRows.forEach(function (r) { r.hidden = true; });
clearBtn.hidden = true;
}
}
function closeModal() {
if (!overlay) return;
document.removeEventListener('keydown', overlay._keyHandler);
overlay.remove();
overlay = null;
}
// Public API
return {
getImage: function () { return imageData; },
getScale: function () { return scale; },
getOffsetX: function () { return offsetX; },
getOffsetY: function () { return offsetY; },
destroy: function () {
closeModal();
if (pasteHandler) {
document.removeEventListener('paste', pasteHandler);
pasteHandler = null;
}
previewEl.removeEventListener('click', openModal);
previewEl.classList.remove('clickable');
}
};
}
return { init: init };
})();