- 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
336 lines
12 KiB
JavaScript
336 lines
12 KiB
JavaScript
// ── 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 };
|
||
})();
|