- Fix: Pull überschreibt lokale Vorlagen nicht mehr (Merge statt Replace) - Fix: toFilename behält Leerzeichen und Groß-/Kleinschreibung - E-Mail in Settings ist jetzt ein Dropdown mit TB-Identitäten - Optionale defaults.local.js für vorkonfigurierte Verbindungsdaten (.gitignored)
1674 lines
62 KiB
JavaScript
1674 lines
62 KiB
JavaScript
// templates_options/templates_options.js
|
||
|
||
const TEMPLATE_STORAGE_KEY = 'message_templates';
|
||
const HASH_STORAGE_KEY = 'sync_hashes';
|
||
const SYNC_CONFIG_KEY = 'gitea_config';
|
||
const SIG_SOURCE_KEY = 'sig_source_map';
|
||
const SIG_FOOTER_KEY = 'sig_footer_cache';
|
||
|
||
// ── Toast, Modal, Offline, Token Toggle ──
|
||
|
||
function showToast(message, type = 'info', duration = 4000) {
|
||
const container = document.getElementById('toast-container');
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast toast-${type}`;
|
||
toast.textContent = message;
|
||
container.appendChild(toast);
|
||
setTimeout(() => {
|
||
toast.classList.add('toast-out');
|
||
setTimeout(() => toast.remove(), 300);
|
||
}, duration);
|
||
}
|
||
|
||
function showConfirmDialog(title, message, confirmText = 'Löschen') {
|
||
return new Promise(resolve => {
|
||
const modal = document.getElementById('confirm-modal');
|
||
document.getElementById('modal-title').textContent = title;
|
||
document.getElementById('modal-message').textContent = message;
|
||
document.getElementById('modal-confirm').textContent = confirmText;
|
||
modal.classList.add('open');
|
||
|
||
function cleanup(result) {
|
||
modal.classList.remove('open');
|
||
document.getElementById('modal-confirm').removeEventListener('click', onConfirm);
|
||
document.getElementById('modal-cancel').removeEventListener('click', onCancel);
|
||
resolve(result);
|
||
}
|
||
function onConfirm() { cleanup(true); }
|
||
function onCancel() { cleanup(false); }
|
||
document.getElementById('modal-confirm').addEventListener('click', onConfirm);
|
||
document.getElementById('modal-cancel').addEventListener('click', onCancel);
|
||
});
|
||
}
|
||
|
||
function checkOnline() {
|
||
if (!navigator.onLine) {
|
||
showToast('Keine Internetverbindung – Synchronisation nicht möglich.', 'error', 5000);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
window.addEventListener('online', () => {
|
||
document.getElementById('offline-banner').classList.remove('visible');
|
||
showToast('Wieder online.', 'success', 2000);
|
||
});
|
||
window.addEventListener('offline', () => {
|
||
document.getElementById('offline-banner').classList.add('visible');
|
||
});
|
||
|
||
// Token toggle + name validation
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const toggle = document.getElementById('token-toggle');
|
||
if (toggle) {
|
||
toggle.addEventListener('click', () => {
|
||
const input = document.getElementById('sync-token');
|
||
const icon = toggle.querySelector('.mdi');
|
||
if (input.type === 'password') {
|
||
input.type = 'text';
|
||
icon.className = 'mdi mdi-eye-off';
|
||
} else {
|
||
input.type = 'password';
|
||
icon.className = 'mdi mdi-eye';
|
||
}
|
||
});
|
||
}
|
||
|
||
// Template name inline validation
|
||
const nameInput = document.getElementById('template-name');
|
||
if (nameInput) {
|
||
nameInput.addEventListener('input', async () => {
|
||
const val = nameInput.value.trim();
|
||
nameInput.classList.remove('input-error', 'input-valid');
|
||
const msgEl = nameInput.parentElement.querySelector('.validation-msg');
|
||
if (msgEl) msgEl.remove();
|
||
|
||
if (!val) {
|
||
nameInput.classList.add('input-error');
|
||
nameInput.insertAdjacentHTML('afterend', '<span class="validation-msg error">Bitte Titel eingeben</span>');
|
||
return;
|
||
}
|
||
|
||
const templates = await getTemplates();
|
||
const editId = document.getElementById('template-id').value;
|
||
const duplicate = templates.find(t => t.name.toLowerCase() === val.toLowerCase() && t.id !== editId);
|
||
if (duplicate) {
|
||
nameInput.classList.add('input-error');
|
||
nameInput.insertAdjacentHTML('afterend', '<span class="validation-msg error">Titel bereits vergeben</span>');
|
||
} else {
|
||
nameInput.classList.add('input-valid');
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// ── Cached author email (for personal folder path) ──
|
||
let cachedAuthorEmail = '';
|
||
|
||
async function getAuthorEmail() {
|
||
if (cachedAuthorEmail) return cachedAuthorEmail;
|
||
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
|
||
cachedAuthorEmail = result[SYNC_CONFIG_KEY]?.authorEmail || '';
|
||
return cachedAuthorEmail;
|
||
}
|
||
|
||
// ── DOM Elements ──
|
||
|
||
const editorPanel = document.getElementById('tpl-editor-panel');
|
||
const templateForm = document.getElementById('template-form');
|
||
const templateList = document.getElementById('templates-list');
|
||
const noTemplatesMessage = document.getElementById('no-templates');
|
||
const saveButton = document.getElementById('save-button');
|
||
const cancelButton = document.getElementById('cancel-edit');
|
||
const formLegend = document.getElementById('form-legend');
|
||
const editorArea = document.getElementById('editor-area');
|
||
|
||
const sigIdentitySelect = document.getElementById('sig-identity-select');
|
||
const sigEditorArea = document.getElementById('sig-editor-area');
|
||
const sigSourceSelect = document.getElementById('sig-source-select');
|
||
const sigSourceInfo = document.getElementById('sig-source-info');
|
||
|
||
// ── System Font Detection ──
|
||
|
||
const FONT_CANDIDATES = [
|
||
'Arial', 'Arial Black', 'Arial Narrow', 'Book Antiqua', 'Bookman Old Style',
|
||
'Calibri', 'Cambria', 'Candara', 'Century Gothic', 'Comic Sans MS',
|
||
'Consolas', 'Constantia', 'Corbel', 'Courier New', 'DejaVu Sans',
|
||
'DejaVu Sans Mono', 'DejaVu Serif', 'Droid Sans', 'Droid Serif',
|
||
'Franklin Gothic Medium', 'Garamond', 'Georgia', 'Gill Sans',
|
||
'Helvetica', 'Helvetica Neue', 'Impact', 'Liberation Mono',
|
||
'Liberation Sans', 'Liberation Serif', 'Lucida Console',
|
||
'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Monaco', 'Noto Sans',
|
||
'Noto Serif', 'Open Sans', 'Palatino Linotype', 'Roboto', 'Segoe UI',
|
||
'Source Sans Pro', 'Tahoma', 'Times New Roman', 'Trebuchet MS',
|
||
'Ubuntu', 'Verdana'
|
||
];
|
||
|
||
const availableFonts = FONT_CANDIDATES.filter(f => document.fonts.check(`12px "${f}"`));
|
||
|
||
const fontInput = document.getElementById('font-input');
|
||
const fontDropdown = document.getElementById('font-dropdown');
|
||
|
||
function renderFontDropdown(filter) {
|
||
fontDropdown.innerHTML = '';
|
||
const q = (filter || '').toLowerCase();
|
||
const matches = q ? availableFonts.filter(f => f.toLowerCase().includes(q)) : availableFonts;
|
||
|
||
for (const font of matches) {
|
||
const div = document.createElement('div');
|
||
div.className = 'font-option';
|
||
div.textContent = font;
|
||
div.style.fontFamily = font;
|
||
div.addEventListener('mousedown', (e) => {
|
||
e.preventDefault();
|
||
fontInput.value = '';
|
||
fontDropdown.classList.remove('open');
|
||
editorArea.focus();
|
||
document.execCommand('fontName', false, font);
|
||
});
|
||
fontDropdown.appendChild(div);
|
||
}
|
||
const isOpen = matches.length > 0;
|
||
fontDropdown.classList.toggle('open', isOpen);
|
||
if (isOpen) {
|
||
requestAnimationFrame(() => {
|
||
const rect = fontDropdown.getBoundingClientRect();
|
||
if (rect.bottom > window.innerHeight) {
|
||
fontDropdown.style.top = 'auto';
|
||
fontDropdown.style.bottom = '100%';
|
||
} else {
|
||
fontDropdown.style.top = '100%';
|
||
fontDropdown.style.bottom = 'auto';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
fontInput.addEventListener('focus', () => renderFontDropdown(fontInput.value));
|
||
fontInput.addEventListener('input', () => renderFontDropdown(fontInput.value));
|
||
fontInput.addEventListener('blur', () => setTimeout(() => fontDropdown.classList.remove('open'), 150));
|
||
fontInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const font = fontInput.value.trim();
|
||
if (!font) return;
|
||
fontInput.value = '';
|
||
fontDropdown.classList.remove('open');
|
||
editorArea.focus();
|
||
document.execCommand('fontName', false, font);
|
||
}
|
||
});
|
||
|
||
// ── Editor Helpers ──
|
||
|
||
function getEditorContent() {
|
||
const html = editorArea.innerHTML;
|
||
return (!html || html === '<br>') ? '' : html;
|
||
}
|
||
|
||
function setEditorContent(html) {
|
||
editorArea.innerHTML = html || '';
|
||
}
|
||
|
||
// ── Toolbar Commands (template editor) ──
|
||
|
||
function setupToolbarCommands(toolbarId, targetEditor) {
|
||
document.querySelectorAll(`#${toolbarId} button[data-cmd]`).forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
targetEditor.focus();
|
||
const cmd = btn.dataset.cmd;
|
||
let val = btn.dataset.val || null;
|
||
|
||
if (val === 'ask') {
|
||
if (cmd === 'createLink') {
|
||
val = prompt('Link-URL eingeben:', 'https://');
|
||
if (!val) return;
|
||
} else if (cmd === 'foreColor' || cmd === 'hiliteColor') {
|
||
const colorInput = document.createElement('input');
|
||
colorInput.type = 'color';
|
||
colorInput.value = cmd === 'foreColor' ? '#000000' : '#ffff00';
|
||
colorInput.addEventListener('input', () => {
|
||
targetEditor.focus();
|
||
document.execCommand(cmd, false, colorInput.value);
|
||
});
|
||
colorInput.click();
|
||
return;
|
||
}
|
||
}
|
||
document.execCommand(cmd, false, val);
|
||
});
|
||
});
|
||
|
||
const fontSizeSelect = document.querySelector(`#${toolbarId} select[data-cmd="fontSize"]`);
|
||
if (fontSizeSelect) {
|
||
fontSizeSelect.addEventListener('change', function() {
|
||
if (!this.value) return;
|
||
targetEditor.focus();
|
||
document.execCommand('fontSize', false, this.value);
|
||
this.value = '';
|
||
});
|
||
}
|
||
}
|
||
|
||
setupToolbarCommands('tpl-toolbar', editorArea);
|
||
setupToolbarCommands('sig-toolbar', sigEditorArea);
|
||
|
||
// ── Signature Font Combo ──
|
||
|
||
const sigFontInput = document.getElementById('sig-font-input');
|
||
const sigFontDropdown = document.getElementById('sig-font-dropdown');
|
||
|
||
function renderSigFontDropdown(filter) {
|
||
sigFontDropdown.innerHTML = '';
|
||
const q = (filter || '').toLowerCase();
|
||
const matches = q ? availableFonts.filter(f => f.toLowerCase().includes(q)) : availableFonts;
|
||
|
||
for (const font of matches) {
|
||
const div = document.createElement('div');
|
||
div.className = 'font-option';
|
||
div.textContent = font;
|
||
div.style.fontFamily = font;
|
||
div.addEventListener('mousedown', (e) => {
|
||
e.preventDefault();
|
||
sigFontInput.value = '';
|
||
sigFontDropdown.classList.remove('open');
|
||
sigEditorArea.focus();
|
||
document.execCommand('fontName', false, font);
|
||
});
|
||
sigFontDropdown.appendChild(div);
|
||
}
|
||
const isOpen = matches.length > 0;
|
||
sigFontDropdown.classList.toggle('open', isOpen);
|
||
if (isOpen) {
|
||
requestAnimationFrame(() => {
|
||
const rect = sigFontDropdown.getBoundingClientRect();
|
||
if (rect.bottom > window.innerHeight) {
|
||
sigFontDropdown.style.top = 'auto';
|
||
sigFontDropdown.style.bottom = '100%';
|
||
} else {
|
||
sigFontDropdown.style.top = '100%';
|
||
sigFontDropdown.style.bottom = 'auto';
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
sigFontInput.addEventListener('focus', () => renderSigFontDropdown(sigFontInput.value));
|
||
sigFontInput.addEventListener('input', () => renderSigFontDropdown(sigFontInput.value));
|
||
sigFontInput.addEventListener('blur', () => setTimeout(() => sigFontDropdown.classList.remove('open'), 150));
|
||
sigFontInput.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') {
|
||
e.preventDefault();
|
||
const font = sigFontInput.value.trim();
|
||
if (!font) return;
|
||
sigFontInput.value = '';
|
||
sigFontDropdown.classList.remove('open');
|
||
sigEditorArea.focus();
|
||
document.execCommand('fontName', false, font);
|
||
}
|
||
});
|
||
|
||
// ── Image Insert ──
|
||
|
||
function setupImageInsert(buttonId, fileInputId, editorEl) {
|
||
document.getElementById(buttonId).addEventListener('click', () => {
|
||
document.getElementById(fileInputId).click();
|
||
});
|
||
document.getElementById(fileInputId).addEventListener('change', (e) => {
|
||
const file = e.target.files[0];
|
||
if (!file) return;
|
||
const reader = new FileReader();
|
||
reader.onload = () => {
|
||
editorEl.focus();
|
||
document.execCommand('insertImage', false, reader.result);
|
||
};
|
||
reader.readAsDataURL(file);
|
||
e.target.value = '';
|
||
});
|
||
}
|
||
|
||
setupImageInsert('tpl-insert-image', 'tpl-image-file', editorArea);
|
||
setupImageInsert('sig-insert-image', 'sig-image-file', sigEditorArea);
|
||
|
||
// ── Tab Navigation ──
|
||
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||
btn.classList.add('active');
|
||
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
||
});
|
||
});
|
||
|
||
// ── Collapsible Import ──
|
||
|
||
document.getElementById('import-toggle').addEventListener('click', function() {
|
||
this.classList.toggle('open');
|
||
document.getElementById('import-body').classList.toggle('open');
|
||
});
|
||
|
||
// ── Template Storage ──
|
||
|
||
async function getTemplates() {
|
||
try {
|
||
const result = await browser.storage.local.get(TEMPLATE_STORAGE_KEY);
|
||
return result[TEMPLATE_STORAGE_KEY] || [];
|
||
} catch (error) {
|
||
console.error("Error retrieving templates:", error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async function saveTemplates(templates) {
|
||
await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
|
||
}
|
||
|
||
// ── Sync Status Tracking ──
|
||
|
||
let tplSyncedHashes = {};
|
||
let sigSyncedHashes = {};
|
||
let lastPulledShas = {};
|
||
let currentRemoteShas = {};
|
||
let allIdentities = [];
|
||
|
||
function simpleHash(str) {
|
||
let h = 0;
|
||
for (let i = 0; i < str.length; i++) {
|
||
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
||
}
|
||
return h;
|
||
}
|
||
|
||
async function loadSyncHashes() {
|
||
try {
|
||
const result = await browser.storage.local.get(HASH_STORAGE_KEY);
|
||
const data = result[HASH_STORAGE_KEY] || {};
|
||
tplSyncedHashes = data.tpl || {};
|
||
sigSyncedHashes = data.sig || {};
|
||
lastPulledShas = data.remoteShas || {};
|
||
currentRemoteShas = { ...lastPulledShas };
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function saveSyncHashes() {
|
||
await browser.storage.local.set({
|
||
[HASH_STORAGE_KEY]: { tpl: tplSyncedHashes, sig: sigSyncedHashes, remoteShas: lastPulledShas }
|
||
});
|
||
}
|
||
|
||
function hasServerUpdate(template) {
|
||
if (!template.remotePath) return false;
|
||
const pulled = lastPulledShas[template.remotePath];
|
||
const remote = currentRemoteShas[template.remotePath];
|
||
if (!pulled || !remote) return false;
|
||
return pulled !== remote;
|
||
}
|
||
|
||
async function checkForServerUpdates() {
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'checkRemoteShas' });
|
||
if (result && result.success) {
|
||
currentRemoteShas = result.remoteShas;
|
||
const templates = await getTemplates();
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
function storeTplHashes(templates) {
|
||
tplSyncedHashes = {};
|
||
for (const t of templates) {
|
||
tplSyncedHashes[t.id] = simpleHash(t.content || '');
|
||
}
|
||
lastPulledShas = { ...currentRemoteShas };
|
||
saveSyncHashes();
|
||
}
|
||
|
||
function getTplSyncClass(template) {
|
||
if (tplSyncedHashes[template.id] === undefined) return 'unknown';
|
||
return simpleHash(template.content || '') === tplSyncedHashes[template.id] ? 'in-sync' : 'out-of-sync';
|
||
}
|
||
|
||
function updateTplSyncIndicator() {
|
||
const el = document.getElementById('tpl-sync-indicator');
|
||
if (!el) return;
|
||
const dot = el.querySelector('.sync-dot');
|
||
const label = el.querySelector('span:last-child');
|
||
|
||
if (Object.keys(tplSyncedHashes).length === 0) {
|
||
dot.className = 'sync-dot unknown';
|
||
label.textContent = 'Sync-Status unbekannt';
|
||
return;
|
||
}
|
||
|
||
getTemplates().then(templates => {
|
||
const outOfSync = templates.filter(t => getTplSyncClass(t) === 'out-of-sync').length;
|
||
const unknown = templates.filter(t => getTplSyncClass(t) === 'unknown').length;
|
||
if (outOfSync > 0) {
|
||
dot.className = 'sync-dot out-of-sync';
|
||
label.textContent = `${outOfSync} Vorlage(n) nicht hochgeladen`;
|
||
} else if (unknown > 0) {
|
||
dot.className = 'sync-dot unknown';
|
||
label.textContent = `${unknown} Vorlage(n) unbekannt`;
|
||
} else {
|
||
dot.className = 'sync-dot in-sync';
|
||
label.textContent = 'Alle Vorlagen synchron';
|
||
}
|
||
});
|
||
}
|
||
|
||
function updateSigSyncIndicator() {
|
||
const el = document.getElementById('sig-sync-indicator');
|
||
if (!el) return;
|
||
const dot = el.querySelector('.sync-dot');
|
||
const label = el.querySelector('span:last-child');
|
||
|
||
if (Object.keys(sigSyncedHashes).length === 0) {
|
||
dot.className = 'sync-dot unknown';
|
||
label.textContent = 'Sync-Status unbekannt';
|
||
return;
|
||
}
|
||
|
||
let outOfSync = 0;
|
||
for (const identity of allIdentities) {
|
||
const email = identity.email.toLowerCase();
|
||
if (sigSyncedHashes[email] !== undefined) {
|
||
if (simpleHash(identity.signature || '') !== sigSyncedHashes[email]) {
|
||
outOfSync++;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (outOfSync > 0) {
|
||
dot.className = 'sync-dot out-of-sync';
|
||
label.textContent = `${outOfSync} Signatur(en) nicht hochgeladen`;
|
||
} else {
|
||
dot.className = 'sync-dot in-sync';
|
||
label.textContent = 'Alle Signaturen synchron';
|
||
}
|
||
}
|
||
|
||
// ── Template List Rendering ──
|
||
|
||
function renderTemplates(templates) {
|
||
templateList.innerHTML = '';
|
||
|
||
if (templates.length === 0) {
|
||
templateList.appendChild(noTemplatesMessage);
|
||
noTemplatesMessage.style.display = 'block';
|
||
return;
|
||
}
|
||
noTemplatesMessage.style.display = 'none';
|
||
|
||
templates.forEach(template => {
|
||
const item = document.createElement('div');
|
||
item.className = 'template-item';
|
||
const syncClass = getTplSyncClass(template);
|
||
const scopeValue = template.folder === '_gemeinsam' ? 'shared'
|
||
: template.folder?.startsWith('_benutzer/') ? 'private' : 'department';
|
||
const badgeClass = scopeValue === 'shared' ? 'badge-shared'
|
||
: scopeValue === 'department' ? 'badge-dept' : 'badge-private';
|
||
const folderBadge = `<select class="scope-select ${badgeClass}" data-id="${template.id}">
|
||
<option value="private" ${scopeValue === 'private' ? 'selected' : ''}>Persönlich</option>
|
||
<option value="department" ${scopeValue === 'department' ? 'selected' : ''}>${document.getElementById('sync-department')?.value || 'Abteilung'}</option>
|
||
<option value="shared" ${scopeValue === 'shared' ? 'selected' : ''}>Alle</option>
|
||
</select>`;
|
||
const pushBtn = syncClass === 'out-of-sync'
|
||
? `<button data-id="${template.id}" class="push-btn" title="Diese Vorlage hochladen">⬆ Hochladen</button>`
|
||
: '';
|
||
const pullBtn = hasServerUpdate(template)
|
||
? `<button data-id="${template.id}" class="pull-btn" title="Neuere Version vom Server laden">⬇ Laden</button>`
|
||
: '';
|
||
item.innerHTML = `
|
||
<span class="sync-dot ${syncClass}" title="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}" aria-label="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}"></span>
|
||
<label class="template-label"><input type="checkbox" class="template-checkbox" data-id="${template.id}"><span class="template-name">${template.name}</span>${folderBadge}</label>
|
||
<div class="template-actions">
|
||
${pullBtn}${pushBtn}
|
||
<button data-id="${template.id}" class="edit-btn">Bearbeiten</button>
|
||
<button data-id="${template.id}" class="delete-btn">Löschen</button>
|
||
</div>
|
||
`;
|
||
templateList.appendChild(item);
|
||
});
|
||
|
||
document.querySelectorAll('.edit-btn').forEach(b => b.addEventListener('click', handleEdit));
|
||
document.querySelectorAll('.delete-btn').forEach(b => b.addEventListener('click', handleDelete));
|
||
document.querySelectorAll('.push-btn').forEach(b => b.addEventListener('click', handlePushSingle));
|
||
document.querySelectorAll('.pull-btn').forEach(b => b.addEventListener('click', handlePullSingle));
|
||
document.querySelectorAll('.scope-select').forEach(sel => sel.addEventListener('change', handleScopeChange));
|
||
}
|
||
|
||
// ── Inline Editor Panel ──
|
||
|
||
function openEditorPanel() {
|
||
editorPanel.classList.add('open');
|
||
// Update department name in scope dropdown
|
||
const deptName = document.getElementById('sync-department')?.value || 'Abteilung';
|
||
const deptOption = document.querySelector('#tpl-scope-select option[value="department"]');
|
||
if (deptOption) deptOption.textContent = deptName;
|
||
editorPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
}
|
||
|
||
function closeEditorPanel() {
|
||
editorPanel.classList.remove('open');
|
||
templateForm.reset();
|
||
document.getElementById('template-id').value = '';
|
||
setEditorContent('');
|
||
formLegend.textContent = 'Neue Vorlage erstellen';
|
||
saveButton.textContent = 'Speichern';
|
||
}
|
||
|
||
document.getElementById('new-template-button').addEventListener('click', () => {
|
||
closeEditorPanel();
|
||
document.getElementById('tpl-scope-select').value = 'private';
|
||
formLegend.textContent = 'Neue Vorlage erstellen';
|
||
saveButton.textContent = 'Speichern';
|
||
openEditorPanel();
|
||
});
|
||
|
||
cancelButton.addEventListener('click', closeEditorPanel);
|
||
|
||
// ── Form Submit (Add/Edit) ──
|
||
|
||
templateForm.addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const id = document.getElementById('template-id').value;
|
||
const name = document.getElementById('template-name').value;
|
||
const content = getEditorContent();
|
||
|
||
if (!content.trim()) {
|
||
showToast('Bitte Inhalt eingeben.', 'error');
|
||
return;
|
||
}
|
||
|
||
let templates = await getTemplates();
|
||
|
||
const scope = document.getElementById('tpl-scope-select').value;
|
||
const authorEmail = await getAuthorEmail();
|
||
let folder;
|
||
if (scope === 'shared') folder = '_gemeinsam';
|
||
else if (scope === 'private') folder = `_benutzer/${authorEmail}`;
|
||
else folder = undefined; // department — pushTemplates sets this.department
|
||
|
||
if (scope === 'private' && !authorEmail) {
|
||
showToast('Bitte E-Mail in den Einstellungen eintragen für persönliche Vorlagen.', 'error');
|
||
return;
|
||
}
|
||
|
||
if (id) {
|
||
const index = templates.findIndex(t => t.id === id);
|
||
if (index > -1) {
|
||
const existingTemplate = templates[index];
|
||
const oldFolder = existingTemplate.folder;
|
||
const scopeChanged = oldFolder !== folder;
|
||
|
||
// Handle scope change: delete old remote file if needed
|
||
if (scopeChanged && existingTemplate.remotePath) {
|
||
const isDowngrade = (oldFolder === '_gemeinsam' && folder !== '_gemeinsam') ||
|
||
(!oldFolder?.startsWith('_benutzer/') && folder?.startsWith('_benutzer/'));
|
||
|
||
if (isDowngrade) {
|
||
const confirmed = await showConfirmDialog(
|
||
'Sichtbarkeit verringern?',
|
||
'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.',
|
||
'Trotzdem ändern'
|
||
);
|
||
if (!confirmed) return;
|
||
}
|
||
try {
|
||
await browser.runtime.sendMessage({
|
||
action: 'deleteRemoteTemplate',
|
||
remotePath: existingTemplate.remotePath
|
||
});
|
||
} catch (_) {}
|
||
existingTemplate.remotePath = undefined;
|
||
}
|
||
|
||
templates[index] = { id, name, content, folder, remotePath: existingTemplate.remotePath };
|
||
}
|
||
} else {
|
||
templates.push({ id: Date.now().toString(), name, content, folder });
|
||
}
|
||
|
||
await saveTemplates(templates);
|
||
renderTemplates(templates);
|
||
closeEditorPanel();
|
||
updateTplSyncIndicator();
|
||
|
||
// Auto-push to server
|
||
try {
|
||
await browser.runtime.sendMessage({ action: 'pushTemplates' });
|
||
const freshTemplates = await getTemplates();
|
||
await checkForServerUpdates();
|
||
storeTplHashes(freshTemplates);
|
||
renderTemplates(freshTemplates);
|
||
updateTplSyncIndicator();
|
||
} catch (_) {}
|
||
});
|
||
|
||
// ── Edit / Delete / Push / Pull ──
|
||
|
||
async function handleEdit(e) {
|
||
const idToEdit = e.target.dataset.id;
|
||
const templates = await getTemplates();
|
||
const template = templates.find(t => t.id === idToEdit);
|
||
if (!template) return;
|
||
|
||
document.getElementById('template-id').value = template.id;
|
||
document.getElementById('template-name').value = template.name;
|
||
const scopeSelect = document.getElementById('tpl-scope-select');
|
||
if (template.folder === '_gemeinsam') scopeSelect.value = 'shared';
|
||
else if (template.folder?.startsWith('_benutzer/')) scopeSelect.value = 'private';
|
||
else scopeSelect.value = 'department';
|
||
setEditorContent(template.content);
|
||
formLegend.textContent = 'Vorlage bearbeiten';
|
||
saveButton.textContent = 'Aktualisieren';
|
||
openEditorPanel();
|
||
}
|
||
|
||
async function handleDelete(e) {
|
||
const id = e.target.dataset.id;
|
||
let templates = await getTemplates();
|
||
const template = templates.find(t => t.id === id);
|
||
if (!template) return;
|
||
|
||
if (template.remotePath) {
|
||
const confirmed = await showConfirmDialog(
|
||
`"${template.name}" löschen?`,
|
||
'Die Vorlage wird lokal und vom Server gelöscht (für alle Nutzer).',
|
||
'Überall löschen'
|
||
);
|
||
if (!confirmed) return;
|
||
|
||
// Delete from server
|
||
try {
|
||
const result = await browser.runtime.sendMessage({
|
||
action: 'deleteRemoteTemplate',
|
||
remotePath: template.remotePath
|
||
});
|
||
if (!result?.success) {
|
||
showToast('Fehler beim Löschen vom Server: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||
}
|
||
} catch (err) {
|
||
showToast('Fehler beim Löschen vom Server: ' + err.message, 'error', 6000);
|
||
}
|
||
} else {
|
||
const confirmed = await showConfirmDialog(
|
||
'Vorlage löschen?',
|
||
`"${template.name}" wirklich löschen?`
|
||
);
|
||
if (!confirmed) return;
|
||
}
|
||
|
||
templates = templates.filter(t => t.id !== id);
|
||
await saveTemplates(templates);
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
showToast('Vorlage gelöscht.', 'info');
|
||
}
|
||
|
||
async function handlePullSingle(e) {
|
||
const btn = e.target.closest('.pull-btn') || e.target;
|
||
const id = btn.dataset.id;
|
||
const templates = await getTemplates();
|
||
const template = templates.find(t => t.id === id);
|
||
if (!template || !template.remotePath) return;
|
||
if (!checkOnline()) return;
|
||
|
||
const origHTML = btn.innerHTML;
|
||
btn.innerHTML = '<span class="spinner"></span>';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const result = await browser.runtime.sendMessage({
|
||
action: 'pullSingleTemplate',
|
||
remotePath: template.remotePath
|
||
});
|
||
if (result && result.success) {
|
||
template.content = result.content;
|
||
await saveTemplates(templates);
|
||
tplSyncedHashes[id] = simpleHash(result.content || '');
|
||
if (template.remotePath && currentRemoteShas[template.remotePath]) {
|
||
lastPulledShas[template.remotePath] = currentRemoteShas[template.remotePath];
|
||
}
|
||
saveSyncHashes();
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
showToast('Vorlage heruntergeladen.', 'success');
|
||
} else {
|
||
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||
btn.innerHTML = origHTML;
|
||
btn.disabled = false;
|
||
}
|
||
} catch (err) {
|
||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||
btn.innerHTML = origHTML;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
async function handlePushSingle(e) {
|
||
const btn = e.target.closest('.push-btn') || e.target;
|
||
const id = btn.dataset.id;
|
||
const templates = await getTemplates();
|
||
const template = templates.find(t => t.id === id);
|
||
if (!template) return;
|
||
if (!checkOnline()) return;
|
||
|
||
const origHTML = btn.innerHTML;
|
||
btn.innerHTML = '<span class="spinner"></span>';
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const result = await browser.runtime.sendMessage({
|
||
action: 'pushSingleTemplate',
|
||
templateId: id
|
||
});
|
||
if (result && result.success) {
|
||
tplSyncedHashes[id] = simpleHash(template.content || '');
|
||
saveSyncHashes();
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
showToast('Vorlage hochgeladen.', 'success');
|
||
} else {
|
||
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||
btn.innerHTML = origHTML;
|
||
btn.disabled = false;
|
||
}
|
||
} catch (err) {
|
||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||
btn.innerHTML = origHTML;
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
|
||
// ── Inline Scope Change ──
|
||
|
||
async function handleScopeChange(e) {
|
||
const id = e.target.dataset.id;
|
||
const newScope = e.target.value;
|
||
let templates = await getTemplates();
|
||
const template = templates.find(t => t.id === id);
|
||
if (!template) return;
|
||
|
||
const authorEmail = await getAuthorEmail();
|
||
const oldFolder = template.folder;
|
||
let newFolder;
|
||
if (newScope === 'shared') newFolder = '_gemeinsam';
|
||
else if (newScope === 'private') newFolder = `_benutzer/${authorEmail}`;
|
||
else newFolder = undefined;
|
||
|
||
if (oldFolder === newFolder) return;
|
||
|
||
if (newScope === 'private' && !authorEmail) {
|
||
showToast('Bitte E-Mail in den Einstellungen eintragen.', 'error');
|
||
// Reset
|
||
e.target.value = oldFolder === '_gemeinsam' ? 'shared'
|
||
: oldFolder?.startsWith('_benutzer/') ? 'private' : 'department';
|
||
return;
|
||
}
|
||
|
||
// Warn on downgrade
|
||
const isDowngrade = (oldFolder === '_gemeinsam' && newFolder !== '_gemeinsam') ||
|
||
(!oldFolder?.startsWith('_benutzer/') && newFolder?.startsWith('_benutzer/'));
|
||
|
||
if (isDowngrade && template.remotePath) {
|
||
const confirmed = await showConfirmDialog(
|
||
'Sichtbarkeit verringern?',
|
||
'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.',
|
||
'Trotzdem ändern'
|
||
);
|
||
if (!confirmed) {
|
||
e.target.value = oldFolder === '_gemeinsam' ? 'shared'
|
||
: oldFolder?.startsWith('_benutzer/') ? 'private' : 'department';
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Delete old remote file if exists
|
||
if (template.remotePath) {
|
||
try {
|
||
await browser.runtime.sendMessage({
|
||
action: 'deleteRemoteTemplate',
|
||
remotePath: template.remotePath
|
||
});
|
||
} catch (_) {}
|
||
template.remotePath = undefined;
|
||
}
|
||
|
||
template.folder = newFolder;
|
||
await saveTemplates(templates);
|
||
|
||
// Auto-push to new location
|
||
try {
|
||
await browser.runtime.sendMessage({ action: 'pushTemplates' });
|
||
templates = await getTemplates();
|
||
} catch (_) {}
|
||
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
showToast('Sichtbarkeit geändert.', 'success');
|
||
}
|
||
|
||
// ── Bulk Actions ──
|
||
|
||
document.getElementById('select-all-button').addEventListener('click', () => {
|
||
const checkboxes = document.querySelectorAll('.template-checkbox');
|
||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||
checkboxes.forEach(cb => cb.checked = !allChecked);
|
||
});
|
||
|
||
document.getElementById('delete-selected-button').addEventListener('click', async () => {
|
||
const checked = document.querySelectorAll('.template-checkbox:checked');
|
||
if (checked.length === 0) return;
|
||
const confirmed = await showConfirmDialog(
|
||
'Ausgewählte löschen?',
|
||
`${checked.length} Vorlage(n) wirklich löschen?`,
|
||
`${checked.length} löschen`
|
||
);
|
||
if (!confirmed) return;
|
||
|
||
const idsToDelete = new Set(Array.from(checked).map(cb => cb.dataset.id));
|
||
let templates = await getTemplates();
|
||
templates = templates.filter(t => !idsToDelete.has(t.id));
|
||
await saveTemplates(templates);
|
||
renderTemplates(templates);
|
||
showToast(`${checked.length} Vorlage(n) gelöscht.`, 'info');
|
||
});
|
||
|
||
// ── HTML File Import ──
|
||
|
||
document.getElementById('import-button').addEventListener('click', async () => {
|
||
const fileInput = document.getElementById('import-files');
|
||
const statusEl = document.getElementById('import-status');
|
||
const files = fileInput.files;
|
||
|
||
if (files.length === 0) {
|
||
showToast('Bitte Dateien auswählen!', 'error');
|
||
return;
|
||
}
|
||
|
||
let templates = await getTemplates();
|
||
let importCount = 0;
|
||
|
||
for (const file of files) {
|
||
const content = await file.text();
|
||
const name = file.name.replace(/\.html?$/i, '');
|
||
let body = content;
|
||
const bodyMatch = content.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||
if (bodyMatch) body = bodyMatch[1].trim();
|
||
|
||
const existingIndex = templates.findIndex(t => t.name.toLowerCase() === name.toLowerCase());
|
||
if (existingIndex > -1) {
|
||
templates[existingIndex].content = body;
|
||
} else {
|
||
templates.push({ id: Date.now().toString() + importCount, name, content: body });
|
||
}
|
||
importCount++;
|
||
}
|
||
|
||
await saveTemplates(templates);
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
|
||
showToast(`${importCount} Vorlage(n) importiert!`, 'success');
|
||
fileInput.value = '';
|
||
});
|
||
|
||
// ── Sync "Aktualisieren" Button (Pull + Push) ──
|
||
|
||
document.getElementById('sync-refresh-button').addEventListener('click', async () => {
|
||
if (!checkOnline()) return;
|
||
const btn = document.getElementById('sync-refresh-button');
|
||
const origText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner spinner-dark"></span> Synchronisiere...';
|
||
|
||
try {
|
||
// Pull first
|
||
const pullResult = await browser.runtime.sendMessage({ action: 'pullTemplates' });
|
||
if (!pullResult?.success) {
|
||
showToast(pullResult?.error || 'Fehler beim Laden', 'error', 6000);
|
||
btn.disabled = false;
|
||
btn.textContent = origText;
|
||
return;
|
||
}
|
||
|
||
// Then push
|
||
const pushResult = await browser.runtime.sendMessage({ action: 'pushTemplates' });
|
||
|
||
const templates = await getTemplates();
|
||
await checkForServerUpdates();
|
||
storeTplHashes(templates);
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
|
||
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
|
||
} catch (err) {
|
||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = origText;
|
||
});
|
||
|
||
// ── Signaturen ──
|
||
|
||
async function getSigSourceMap() {
|
||
const result = await browser.storage.local.get(SIG_SOURCE_KEY);
|
||
return result[SIG_SOURCE_KEY] || {};
|
||
}
|
||
|
||
async function setSigSource(email, source) {
|
||
const map = await getSigSourceMap();
|
||
map[email.toLowerCase()] = source;
|
||
await browser.storage.local.set({ [SIG_SOURCE_KEY]: map });
|
||
}
|
||
|
||
function updateSigSourceDropdown(currentEmail) {
|
||
sigSourceSelect.innerHTML = '';
|
||
|
||
const optOwn = document.createElement('option');
|
||
optOwn.value = 'own';
|
||
optOwn.textContent = 'Eigene Signatur';
|
||
sigSourceSelect.appendChild(optOwn);
|
||
|
||
for (const id of allIdentities) {
|
||
if (id.email.toLowerCase() === currentEmail.toLowerCase()) continue;
|
||
const opt = document.createElement('option');
|
||
opt.value = '=' + id.email.toLowerCase();
|
||
opt.textContent = `= ${id.email}`;
|
||
sigSourceSelect.appendChild(opt);
|
||
}
|
||
}
|
||
|
||
function updateSigEditorState(source) {
|
||
const isEditable = (source === 'own');
|
||
sigEditorArea.contentEditable = isEditable ? 'true' : 'false';
|
||
sigEditorArea.style.opacity = isEditable ? '1' : '0.6';
|
||
sigEditorArea.style.pointerEvents = isEditable ? 'auto' : 'none';
|
||
document.querySelectorAll('#sig-toolbar button, #sig-toolbar select').forEach(el => {
|
||
el.disabled = !isEditable;
|
||
el.style.opacity = isEditable ? '1' : '0.4';
|
||
});
|
||
|
||
if (source === 'own') {
|
||
sigSourceInfo.textContent = 'Eigene Signatur — wird im Editor bearbeitet.';
|
||
} else if (source.startsWith('=')) {
|
||
sigSourceInfo.textContent = `Übernimmt die Signatur von ${source.substring(1)}.`;
|
||
}
|
||
}
|
||
|
||
async function loadIdentities() {
|
||
allIdentities = [];
|
||
sigIdentitySelect.innerHTML = '';
|
||
|
||
const accounts = await browser.accounts.list();
|
||
for (const account of accounts) {
|
||
const identities = await browser.identities.list(account.id);
|
||
for (const identity of identities) {
|
||
const label = identity.name
|
||
? `${identity.name} <${identity.email}>`
|
||
: identity.email;
|
||
allIdentities.push({
|
||
id: identity.id,
|
||
email: identity.email,
|
||
label,
|
||
accountName: account.name,
|
||
signature: identity.signature || '',
|
||
signatureIsPlainText: identity.signatureIsPlainText || false
|
||
});
|
||
const opt = document.createElement('option');
|
||
opt.value = identity.id;
|
||
opt.textContent = `${label} (${account.name})`;
|
||
sigIdentitySelect.appendChild(opt);
|
||
}
|
||
}
|
||
|
||
// Auto-select first identity and load signature directly
|
||
if (allIdentities.length > 0) {
|
||
const first = allIdentities[0];
|
||
sigIdentitySelect.value = first.id;
|
||
|
||
const sourceMap = await getSigSourceMap();
|
||
const source = sourceMap[first.email.toLowerCase()] || 'own';
|
||
updateSigSourceDropdown(first.email);
|
||
sigSourceSelect.value = source;
|
||
|
||
if (source.startsWith('=')) {
|
||
const srcEmail = source.substring(1);
|
||
const srcIdentity = allIdentities.find(i => i.email.toLowerCase() === srcEmail);
|
||
sigEditorArea.innerHTML = extractHeader(srcIdentity ? (srcIdentity.signature || '') : '');
|
||
} else {
|
||
sigEditorArea.innerHTML = extractHeader(first.signature || '');
|
||
}
|
||
updateSigEditorState(source);
|
||
}
|
||
}
|
||
|
||
// Load signature header into editor when identity is selected
|
||
sigIdentitySelect.addEventListener('change', async () => {
|
||
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
|
||
if (identity) {
|
||
const sourceMap = await getSigSourceMap();
|
||
const source = sourceMap[identity.email.toLowerCase()] || 'own';
|
||
|
||
updateSigSourceDropdown(identity.email);
|
||
sigSourceSelect.value = source;
|
||
|
||
if (source.startsWith('=')) {
|
||
const srcEmail = source.substring(1);
|
||
const srcIdentity = allIdentities.find(i => i.email.toLowerCase() === srcEmail);
|
||
sigEditorArea.innerHTML = extractHeader(srcIdentity ? (srcIdentity.signature || '') : '');
|
||
} else {
|
||
sigEditorArea.innerHTML = extractHeader(identity.signature || '');
|
||
}
|
||
updateSigEditorState(source);
|
||
} else {
|
||
sigEditorArea.innerHTML = '';
|
||
sigSourceSelect.innerHTML = '<option value="own">Eigene Signatur</option>';
|
||
sigSourceInfo.textContent = '';
|
||
}
|
||
});
|
||
|
||
sigSourceSelect.addEventListener('change', async () => {
|
||
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
|
||
if (!identity) return;
|
||
|
||
const source = sigSourceSelect.value;
|
||
await setSigSource(identity.email, source);
|
||
updateSigEditorState(source);
|
||
|
||
if (source.startsWith('=')) {
|
||
const srcEmail = source.substring(1);
|
||
const srcIdentity = allIdentities.find(i => i.email.toLowerCase() === srcEmail);
|
||
sigEditorArea.innerHTML = extractHeader(srcIdentity ? (srcIdentity.signature || '') : '');
|
||
} else {
|
||
sigEditorArea.innerHTML = extractHeader(identity.signature || '');
|
||
}
|
||
});
|
||
|
||
// ── Signature Header/Footer Helpers ──
|
||
|
||
// The footer separator comment in the combined signature
|
||
const FOOTER_SEPARATOR = '<!-- SIG_FOOTER_START -->';
|
||
|
||
function extractHeader(fullSignature) {
|
||
const idx = fullSignature.indexOf(FOOTER_SEPARATOR);
|
||
if (idx === -1) return fullSignature;
|
||
return fullSignature.substring(0, idx).trim();
|
||
}
|
||
|
||
async function getFooter() {
|
||
// Try to get fresh footer from server, fall back to cache
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
|
||
if (result && result.success && result.html) {
|
||
return result.html;
|
||
}
|
||
} catch (_) {}
|
||
// Fallback: cached version
|
||
const cached = await browser.storage.local.get(SIG_FOOTER_KEY);
|
||
return cached[SIG_FOOTER_KEY] || '';
|
||
}
|
||
|
||
function combineSignature(header, footer) {
|
||
if (!footer) return header;
|
||
return header + '\n' + FOOTER_SEPARATOR + '\n' + footer;
|
||
}
|
||
|
||
// ── "Vorlage laden" Button ──
|
||
|
||
document.getElementById('sig-load-template').addEventListener('click', async () => {
|
||
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
|
||
if (!identity) {
|
||
showSigStatus('Bitte zuerst eine Identität auswählen.', 'red');
|
||
return;
|
||
}
|
||
|
||
showSigStatus('Lade Vorlage...', '#777');
|
||
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'loadSignatureTemplate' });
|
||
if (result && result.success) {
|
||
let html = result.html;
|
||
|
||
// Replace placeholders
|
||
const authorName = document.getElementById('sync-author-name')?.value || '';
|
||
const department = document.getElementById('sync-department')?.value || '';
|
||
html = html.replace(/\{\{NAME\}\}/g, authorName || '{{NAME}}');
|
||
html = html.replace(/\{\{EMAIL\}\}/g, identity.email);
|
||
html = html.replace(/\{\{ABTEILUNG\}\}/g, department || '{{ABTEILUNG}}');
|
||
html = html.replace(/\{\{TELEFON\}\}/g, '+49 (0) 5191 - 605-0');
|
||
html = html.replace(/\{\{FAX\}\}/g, '+49 (0) 5191 - 605-185');
|
||
|
||
sigEditorArea.innerHTML = html;
|
||
|
||
// Set source to "own" since they're now editing
|
||
await setSigSource(identity.email, 'own');
|
||
sigSourceSelect.value = 'own';
|
||
updateSigEditorState('own');
|
||
|
||
showSigStatus('Vorlage geladen — bitte Abteilung und Telefon ausfüllen.', '#555');
|
||
} else {
|
||
showSigStatus(result?.error || 'Vorlage nicht gefunden', 'red');
|
||
}
|
||
} catch (err) {
|
||
showSigStatus('Fehler: ' + err.message, 'red');
|
||
}
|
||
});
|
||
|
||
function showSigStatus(message, color) {
|
||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||
}
|
||
|
||
// Save signature to Thunderbird identity (header + footer)
|
||
document.getElementById('sig-save-button').addEventListener('click', async () => {
|
||
const identityId = sigIdentitySelect.value;
|
||
if (!identityId) {
|
||
showSigStatus('Bitte Identität auswählen.', 'red');
|
||
return;
|
||
}
|
||
|
||
const identity = allIdentities.find(i => i.id === identityId);
|
||
if (!identity) return;
|
||
|
||
const sourceMap = await getSigSourceMap();
|
||
const source = sourceMap[identity.email.toLowerCase()] || 'own';
|
||
|
||
let header;
|
||
if (source.startsWith('=')) {
|
||
const srcEmail = source.substring(1);
|
||
const srcIdentity = allIdentities.find(i => i.email.toLowerCase() === srcEmail);
|
||
header = extractHeader(srcIdentity ? (srcIdentity.signature || '') : '');
|
||
} else {
|
||
header = sigEditorArea.innerHTML;
|
||
}
|
||
|
||
const footer = await getFooter();
|
||
const fullSignature = combineSignature(header, footer);
|
||
|
||
await browser.identities.update(identityId, {
|
||
signature: fullSignature,
|
||
signatureIsPlainText: false
|
||
});
|
||
|
||
if (identity) identity.signature = fullSignature;
|
||
updateSigSyncIndicator();
|
||
|
||
// Auto-push to server
|
||
try {
|
||
const pushResult = await browser.runtime.sendMessage({ action: 'pushSignatures' });
|
||
await loadIdentities();
|
||
for (const id of allIdentities) {
|
||
sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
|
||
}
|
||
saveSyncHashes();
|
||
updateSigSyncIndicator();
|
||
const label = source.startsWith('=') ? ` (von ${source.substring(1)})` : '';
|
||
showSigStatus(`Signatur gespeichert & hochgeladen!${label}`, 'green');
|
||
} catch (err) {
|
||
showSigStatus('Gespeichert, aber Upload fehlgeschlagen: ' + err.message, '#e65100');
|
||
}
|
||
});
|
||
|
||
// Import signature from HTML file
|
||
document.getElementById('sig-import-file-btn').addEventListener('click', () => {
|
||
document.getElementById('sig-import-file').click();
|
||
});
|
||
|
||
document.getElementById('sig-import-file').addEventListener('change', async (e) => {
|
||
const files = Array.from(e.target.files);
|
||
if (files.length === 0) return;
|
||
|
||
const htmlFile = files.find(f => /\.html?$/i.test(f.name));
|
||
const imageFiles = files.filter(f => f.type.startsWith('image/'));
|
||
|
||
if (!htmlFile) {
|
||
showSigStatus('Bitte eine HTML-Datei auswählen.', 'red');
|
||
e.target.value = '';
|
||
return;
|
||
}
|
||
|
||
const imageMap = {};
|
||
for (const img of imageFiles) {
|
||
const dataUri = await new Promise(resolve => {
|
||
const reader = new FileReader();
|
||
reader.onload = () => resolve(reader.result);
|
||
reader.readAsDataURL(img);
|
||
});
|
||
imageMap[img.name.toLowerCase()] = dataUri;
|
||
}
|
||
|
||
let html = await htmlFile.text();
|
||
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||
if (bodyMatch) html = bodyMatch[1].trim();
|
||
|
||
html = html.replace(/<img\s+([^>]*?)src=["']([^"']+)["']/gi, (match, before, src) => {
|
||
const filename = src.split('/').pop().split('\\').pop().toLowerCase();
|
||
if (imageMap[filename]) return `<img ${before}src="${imageMap[filename]}"`;
|
||
return match;
|
||
});
|
||
|
||
const unresolvedImages = (html.match(/<img[^>]+src=["'](?!data:)[^"']+["']/gi) || []).length;
|
||
sigEditorArea.innerHTML = html;
|
||
e.target.value = '';
|
||
|
||
if (unresolvedImages > 0) {
|
||
showSigStatus(`Geladen — ${unresolvedImages} Bild(er) nicht gefunden.`, '#e65100');
|
||
} else {
|
||
showSigStatus(`Geladen${imageFiles.length ? ` — ${imageFiles.length} Bild(er) eingebettet` : ''}. Jetzt speichern.`, '#555');
|
||
}
|
||
});
|
||
|
||
// Signature sync - "Aktualisieren" (pull + push)
|
||
document.getElementById('sig-sync-refresh').addEventListener('click', async () => {
|
||
if (!checkOnline()) return;
|
||
const btn = document.getElementById('sig-sync-refresh');
|
||
const origText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner spinner-dark"></span> Synchronisiere...';
|
||
|
||
try {
|
||
// Pull first (gets footer + headers)
|
||
const pullResult = await browser.runtime.sendMessage({ action: 'pullSignatures' });
|
||
if (!pullResult?.success) {
|
||
showToast(pullResult?.error || 'Fehler', 'error', 6000);
|
||
btn.disabled = false;
|
||
btn.textContent = origText;
|
||
return;
|
||
}
|
||
|
||
// Then push
|
||
const pushResult = await browser.runtime.sendMessage({ action: 'pushSignatures' });
|
||
|
||
await loadIdentities();
|
||
for (const id of allIdentities) {
|
||
sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
|
||
}
|
||
saveSyncHashes();
|
||
updateSigSyncIndicator();
|
||
sigIdentitySelect.dispatchEvent(new Event('change'));
|
||
|
||
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
|
||
} catch (err) {
|
||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = origText;
|
||
});
|
||
|
||
// ── Footer Editor ──
|
||
|
||
const footerEditorArea = document.getElementById('footer-editor-area');
|
||
|
||
setupToolbarCommands('footer-toolbar', footerEditorArea);
|
||
setupImageInsert('footer-insert-image', 'footer-image-file', footerEditorArea);
|
||
|
||
document.getElementById('footer-toggle').addEventListener('click', async function() {
|
||
this.classList.toggle('open');
|
||
document.getElementById('footer-body').classList.toggle('open');
|
||
|
||
// Auto-load footer when opening and editor is empty
|
||
if (this.classList.contains('open') && (!footerEditorArea.innerHTML || footerEditorArea.innerHTML === '<br>')) {
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
|
||
if (result && result.success && result.html) {
|
||
footerEditorArea.innerHTML = result.html;
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
});
|
||
|
||
function showFooterStatus(message, color) {
|
||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||
}
|
||
|
||
// Load footer from server
|
||
document.getElementById('footer-load-button').addEventListener('click', async () => {
|
||
showFooterStatus('Lade...', '#777');
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
|
||
if (result && result.success) {
|
||
footerEditorArea.innerHTML = result.html || '';
|
||
showFooterStatus(result.html ? 'Fußbereich geladen.' : 'Kein Fußbereich für diese Abteilung gefunden.', result.html ? 'green' : '#e65100');
|
||
} else {
|
||
showFooterStatus(result?.error || 'Fehler', 'red');
|
||
}
|
||
} catch (err) {
|
||
showFooterStatus('Fehler: ' + err.message, 'red');
|
||
}
|
||
});
|
||
|
||
// Save & push footer
|
||
document.getElementById('footer-save-button').addEventListener('click', async () => {
|
||
const html = footerEditorArea.innerHTML;
|
||
if (!html || html === '<br>') {
|
||
showFooterStatus('Fußbereich ist leer.', 'red');
|
||
return;
|
||
}
|
||
|
||
showFooterStatus('Speichere...', '#777');
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'pushFooter', html });
|
||
if (result && result.success) {
|
||
showFooterStatus('Fußbereich gespeichert & hochgeladen!', 'green');
|
||
} else {
|
||
showFooterStatus(result?.error || 'Fehler', 'red');
|
||
}
|
||
} catch (err) {
|
||
showFooterStatus('Fehler: ' + err.message, 'red');
|
||
}
|
||
});
|
||
|
||
// ── Sync Settings UI ──
|
||
|
||
async function populateEmailDropdown() {
|
||
const select = document.getElementById('sync-author-email');
|
||
const accounts = await browser.accounts.list();
|
||
const emails = new Set();
|
||
for (const account of accounts) {
|
||
for (const identity of account.identities) {
|
||
if (identity.email && !emails.has(identity.email)) {
|
||
emails.add(identity.email);
|
||
const opt = document.createElement('option');
|
||
opt.value = identity.email;
|
||
opt.textContent = identity.email;
|
||
select.appendChild(opt);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
async function loadSyncConfig() {
|
||
try {
|
||
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
|
||
let config = result[SYNC_CONFIG_KEY];
|
||
|
||
// First launch: apply defaults from defaults.local.js if available
|
||
if (!config && typeof DEFAULT_SYNC_CONFIG !== 'undefined') {
|
||
config = { ...DEFAULT_SYNC_CONFIG };
|
||
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
|
||
}
|
||
|
||
if (config) {
|
||
document.getElementById('sync-url').value = config.baseUrl || '';
|
||
document.getElementById('sync-owner').value = config.owner || '';
|
||
document.getElementById('sync-repo').value = config.repo || '';
|
||
document.getElementById('sync-branch').value = config.branch || 'main';
|
||
document.getElementById('sync-token').value = config.token || '';
|
||
document.getElementById('sync-author-name').value = config.authorName || '';
|
||
document.getElementById('sync-author-email').value = config.authorEmail || '';
|
||
cachedAuthorEmail = config.authorEmail || '';
|
||
if (config.baseUrl && config.token) {
|
||
updateSyncStatus('connected', 'Verbunden');
|
||
}
|
||
if (config.department) {
|
||
const deptSelect = document.getElementById('sync-department');
|
||
const opt = document.createElement('option');
|
||
opt.value = config.department;
|
||
opt.textContent = config.department;
|
||
opt.selected = true;
|
||
deptSelect.appendChild(opt);
|
||
}
|
||
loadDepartments();
|
||
|
||
// Auto-detect department + author from server config
|
||
if (config.baseUrl && config.token) {
|
||
tryAutoDetect(config);
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
async function tryAutoDetect(currentConfig) {
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'autoDetect' });
|
||
if (!result?.success || !result.config) return;
|
||
|
||
// result.config = { "info@hotel.de": "Rezeption", ... }
|
||
const deptMap = result.config;
|
||
|
||
// Get all Thunderbird identities
|
||
const accounts = await browser.accounts.list();
|
||
const tbEmails = [];
|
||
for (const account of accounts) {
|
||
for (const identity of account.identities) {
|
||
tbEmails.push(identity.email.toLowerCase());
|
||
}
|
||
}
|
||
|
||
// Find matching department email
|
||
let detectedDept = null;
|
||
let personalEmail = null;
|
||
const matchedEmails = [];
|
||
const unmatchedEmails = [];
|
||
|
||
for (const email of tbEmails) {
|
||
if (deptMap[email]) {
|
||
detectedDept = deptMap[email];
|
||
matchedEmails.push(email);
|
||
} else {
|
||
unmatchedEmails.push(email);
|
||
}
|
||
}
|
||
|
||
// The personal email is the one NOT in the department map
|
||
if (unmatchedEmails.length > 0) {
|
||
personalEmail = unmatchedEmails[0];
|
||
}
|
||
|
||
let changed = false;
|
||
|
||
// Auto-set department if not already set
|
||
if (detectedDept && !currentConfig.department) {
|
||
const deptSelect = document.getElementById('sync-department');
|
||
// Add option if not exists
|
||
let found = false;
|
||
for (const opt of deptSelect.options) {
|
||
if (opt.value === detectedDept) { opt.selected = true; found = true; break; }
|
||
}
|
||
if (!found) {
|
||
const opt = document.createElement('option');
|
||
opt.value = detectedDept;
|
||
opt.textContent = detectedDept;
|
||
opt.selected = true;
|
||
deptSelect.appendChild(opt);
|
||
}
|
||
currentConfig.department = detectedDept;
|
||
changed = true;
|
||
}
|
||
|
||
// Auto-set author email if not already set
|
||
if (personalEmail && !currentConfig.authorEmail) {
|
||
document.getElementById('sync-author-email').value = personalEmail;
|
||
currentConfig.authorEmail = personalEmail;
|
||
cachedAuthorEmail = personalEmail;
|
||
changed = true;
|
||
}
|
||
|
||
// Auto-set author name from TB identity if not already set
|
||
if (personalEmail && !currentConfig.authorName) {
|
||
for (const account of accounts) {
|
||
for (const identity of account.identities) {
|
||
if (identity.email.toLowerCase() === personalEmail && identity.name) {
|
||
document.getElementById('sync-author-name').value = identity.name;
|
||
currentConfig.authorName = identity.name;
|
||
changed = true;
|
||
break;
|
||
}
|
||
}
|
||
if (changed && currentConfig.authorName) break;
|
||
}
|
||
}
|
||
|
||
if (changed) {
|
||
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: currentConfig });
|
||
showToast('Einstellungen automatisch erkannt.', 'info');
|
||
}
|
||
} catch (err) {
|
||
console.log('Auto-detect failed (optional):', err.message);
|
||
}
|
||
}
|
||
|
||
function getSyncConfigFromForm() {
|
||
return {
|
||
baseUrl: document.getElementById('sync-url').value.replace(/\/+$/, ''),
|
||
owner: document.getElementById('sync-owner').value.trim(),
|
||
repo: document.getElementById('sync-repo').value.trim(),
|
||
branch: document.getElementById('sync-branch').value.trim() || 'main',
|
||
token: document.getElementById('sync-token').value.trim(),
|
||
authorName: document.getElementById('sync-author-name').value.trim(),
|
||
authorEmail: document.getElementById('sync-author-email').value.trim(),
|
||
department: document.getElementById('sync-department').value
|
||
};
|
||
}
|
||
|
||
function updateSyncStatus(type, message) {
|
||
const bar = document.getElementById('sync-status-bar');
|
||
bar.className = 'sync-status ' + type;
|
||
bar.textContent = message;
|
||
}
|
||
|
||
function showSyncActionStatus(elId, message, color) {
|
||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||
}
|
||
|
||
function appendSyncLog(message) {
|
||
const log = document.getElementById('sync-log');
|
||
log.style.display = 'block';
|
||
const time = new Date().toLocaleTimeString('de-DE');
|
||
log.textContent += `[${time}] ${message}\n`;
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
|
||
async function loadDepartments() {
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'listDepartments' });
|
||
if (result && result.success) {
|
||
const select = document.getElementById('sync-department');
|
||
// Get saved department from config, not just DOM (more reliable)
|
||
const configResult = await browser.storage.local.get(SYNC_CONFIG_KEY);
|
||
const savedDept = configResult[SYNC_CONFIG_KEY]?.department || select.value || '';
|
||
select.innerHTML = '<option value="">— Bitte wählen —</option>';
|
||
for (const dept of result.departments) {
|
||
const opt = document.createElement('option');
|
||
opt.value = dept;
|
||
opt.textContent = dept;
|
||
if (dept === savedDept) opt.selected = true;
|
||
select.appendChild(opt);
|
||
}
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
|
||
document.getElementById('save-sync-config').addEventListener('click', async () => {
|
||
const config = getSyncConfigFromForm();
|
||
if (!config.baseUrl || !config.owner || !config.repo || !config.token) {
|
||
showSyncActionStatus('sync-action-status', 'Bitte alle Verbindungsfelder ausfüllen.', 'red');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const origin = new URL(config.baseUrl).origin + '/*';
|
||
const granted = await browser.permissions.request({ origins: [origin] });
|
||
if (!granted) {
|
||
showSyncActionStatus('sync-action-status', 'Berechtigung für Server abgelehnt.', 'red');
|
||
return;
|
||
}
|
||
} catch (err) {
|
||
showSyncActionStatus('sync-action-status', 'Ungültige URL.', 'red');
|
||
return;
|
||
}
|
||
|
||
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
|
||
updateSyncStatus('connected', 'Verbunden');
|
||
showSyncActionStatus('sync-action-status', 'Gespeichert!', 'green');
|
||
appendSyncLog('Verbindung konfiguriert.');
|
||
loadDepartments();
|
||
});
|
||
|
||
document.getElementById('sync-department').addEventListener('change', async () => {
|
||
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
|
||
const config = result[SYNC_CONFIG_KEY] || {};
|
||
config.department = document.getElementById('sync-department').value;
|
||
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
|
||
|
||
if (config.department) {
|
||
try {
|
||
// Pull templates for new department
|
||
const pullResult = await browser.runtime.sendMessage({ action: 'pullTemplates' });
|
||
if (pullResult && pullResult.success) {
|
||
const templates = await getTemplates();
|
||
renderTemplates(templates);
|
||
storeTplHashes(templates);
|
||
updateTplSyncIndicator();
|
||
appendSyncLog(`Abteilung gewechselt: ${config.department} — ${pullResult.updated || 0} Vorlage(n) geladen.`);
|
||
}
|
||
|
||
// Pull new department footer
|
||
await browser.runtime.sendMessage({ action: 'pullSignatures' });
|
||
// Clear footer editor so it reloads on next open
|
||
if (footerEditorArea) footerEditorArea.innerHTML = '';
|
||
} catch (_) {}
|
||
}
|
||
});
|
||
|
||
for (const id of ['sync-author-name', 'sync-author-email']) {
|
||
document.getElementById(id).addEventListener('change', async () => {
|
||
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
|
||
const config = result[SYNC_CONFIG_KEY] || {};
|
||
config.authorName = document.getElementById('sync-author-name').value.trim();
|
||
config.authorEmail = document.getElementById('sync-author-email').value.trim();
|
||
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
|
||
});
|
||
}
|
||
|
||
document.getElementById('refresh-departments').addEventListener('click', loadDepartments);
|
||
|
||
document.getElementById('test-sync-connection').addEventListener('click', async () => {
|
||
if (!checkOnline()) return;
|
||
const btn = document.getElementById('test-sync-connection');
|
||
const origText = btn.textContent;
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spinner spinner-dark"></span> Teste...';
|
||
|
||
try {
|
||
const result = await browser.runtime.sendMessage({ action: 'testConnection' });
|
||
if (result && result.success) {
|
||
updateSyncStatus('connected', 'Verbunden');
|
||
showToast('Verbindung erfolgreich!', 'success');
|
||
appendSyncLog('Verbindungstest erfolgreich.');
|
||
loadDepartments();
|
||
} else {
|
||
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
||
showToast(result?.error || 'Fehler', 'error', 6000);
|
||
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
|
||
}
|
||
} catch (err) {
|
||
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
||
showToast('Sync nicht verfügbar.', 'error', 6000);
|
||
}
|
||
btn.disabled = false;
|
||
btn.textContent = origText;
|
||
});
|
||
|
||
// ── Init ──
|
||
|
||
window.addEventListener('load', async () => {
|
||
await loadSyncHashes();
|
||
const templates = await getTemplates();
|
||
renderTemplates(templates);
|
||
updateTplSyncIndicator();
|
||
await populateEmailDropdown();
|
||
loadSyncConfig();
|
||
await loadIdentities();
|
||
|
||
checkForServerUpdates();
|
||
setInterval(checkForServerUpdates, 30000);
|
||
});
|