// 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';
// ── 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);
}
fontDropdown.classList.toggle('open', matches.length > 0);
}
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 === '
') ? '' : 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);
}
sigFontDropdown.classList.toggle('open', matches.length > 0);
}
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 folderBadge = template.folder
? `[${template.folder}]`
: '';
const pushBtn = syncClass === 'out-of-sync'
? ``
: '';
const pullBtn = hasServerUpdate(template)
? ``
: '';
item.innerHTML = `
${template.name}${folderBadge}
${pullBtn}${pushBtn}
`;
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));
}
// ── Inline Editor Panel ──
function openEditorPanel() {
editorPanel.classList.add('open');
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-shared-toggle').checked = false;
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()) {
alert('Bitte Inhalt eingeben.');
return;
}
let templates = await getTemplates();
const isShared = document.getElementById('tpl-shared-toggle').checked;
const folder = isShared ? '_gemeinsam' : undefined;
if (id) {
const index = templates.findIndex(t => t.id === id);
if (index > -1) {
const remotePath = templates[index].remotePath;
const effectiveFolder = folder !== undefined ? folder : templates[index].folder;
templates[index] = { id, name, content, folder: effectiveFolder, remotePath };
}
} else {
templates.push({ id: Date.now().toString(), name, content, folder: folder || undefined });
}
await saveTemplates(templates);
renderTemplates(templates);
closeEditorPanel();
updateTplSyncIndicator();
});
// ── 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;
document.getElementById('tpl-shared-toggle').checked = (template.folder === '_gemeinsam');
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 choice = confirm(
`"${template.name}" löschen?\n\n` +
`OK = Lokal UND vom Server löschen (für alle)\n` +
`Abbrechen = Nicht löschen`
);
if (!choice) return;
// Delete from server
try {
const result = await browser.runtime.sendMessage({
action: 'deleteRemoteTemplate',
remotePath: template.remotePath
});
if (!result?.success) {
alert('Fehler beim Löschen vom Server: ' + (result?.error || 'Unbekannt'));
}
} catch (err) {
alert('Fehler beim Löschen vom Server: ' + err.message);
}
} else {
if (!confirm('Diese Vorlage wirklich löschen?')) return;
}
templates = templates.filter(t => t.id !== id);
await saveTemplates(templates);
renderTemplates(templates);
updateTplSyncIndicator();
}
async function handlePullSingle(e) {
const id = e.target.dataset.id;
const templates = await getTemplates();
const template = templates.find(t => t.id === id);
if (!template || !template.remotePath) return;
e.target.textContent = '...';
e.target.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();
} else {
alert('Fehler: ' + (result?.error || 'Unbekannt'));
e.target.textContent = '\u2193';
e.target.disabled = false;
}
} catch (err) {
alert('Fehler: ' + err.message);
e.target.textContent = '\u2193';
e.target.disabled = false;
}
}
async function handlePushSingle(e) {
const id = e.target.dataset.id;
const templates = await getTemplates();
const template = templates.find(t => t.id === id);
if (!template) return;
e.target.textContent = '...';
e.target.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();
} else {
alert('Fehler: ' + (result?.error || 'Unbekannt'));
e.target.textContent = '\u2191';
e.target.disabled = false;
}
} catch (err) {
alert('Fehler: ' + err.message);
e.target.textContent = '\u2191';
e.target.disabled = false;
}
}
// ── 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;
if (!confirm(`${checked.length} Vorlage(n) wirklich löschen?`)) 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);
});
// ── 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) {
statusEl.textContent = 'Bitte Dateien auswählen!';
statusEl.style.color = 'red';
statusEl.style.display = 'inline';
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(/]*>([\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();
statusEl.textContent = `${importCount} Vorlage(n) importiert!`;
statusEl.style.color = 'green';
statusEl.style.display = 'inline';
fileInput.value = '';
setTimeout(() => { statusEl.style.display = 'none'; }, 3000);
});
// ── Sync "Aktualisieren" Button (Pull + Push) ──
document.getElementById('sync-refresh-button').addEventListener('click', async () => {
const statusEl = document.getElementById('sync-sync-status');
statusEl.textContent = 'Synchronisiere...';
statusEl.style.color = '#777';
statusEl.style.display = 'inline';
try {
// Pull first
const pullResult = await browser.runtime.sendMessage({ action: 'pullTemplates' });
if (!pullResult?.success) {
statusEl.textContent = pullResult?.error || 'Fehler beim Laden';
statusEl.style.color = 'red';
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
return;
}
// Then push
const pushResult = await browser.runtime.sendMessage({ action: 'pushTemplates' });
const templates = await getTemplates();
await checkForServerUpdates();
storeTplHashes(templates);
renderTemplates(templates);
updateTplSyncIndicator();
const msg = `${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`;
statusEl.textContent = msg;
statusEl.style.color = 'green';
} catch (err) {
statusEl.textContent = 'Fehler: ' + err.message;
statusEl.style.color = 'red';
}
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
});
// ── 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);
}
}
}
// 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 = '';
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 = '';
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 el = document.getElementById('sig-status');
el.textContent = message;
el.style.color = color;
el.style.display = 'inline';
setTimeout(() => { el.style.display = 'none'; }, 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(/]*>([\s\S]*)<\/body>/i);
if (bodyMatch) html = bodyMatch[1].trim();
html = html.replace(/
]*?)src=["']([^"']+)["']/gi, (match, before, src) => {
const filename = src.split('/').pop().split('\\').pop().toLowerCase();
if (imageMap[filename]) return `
]+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 () => {
const statusEl = document.getElementById('sig-sync-status');
statusEl.textContent = 'Synchronisiere...';
statusEl.style.color = '#777';
statusEl.style.display = 'inline';
try {
// Pull first (gets footer + headers)
const pullResult = await browser.runtime.sendMessage({ action: 'pullSignatures' });
if (!pullResult?.success) {
statusEl.textContent = pullResult?.error || 'Fehler';
statusEl.style.color = 'red';
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
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'));
const msg = `${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`;
statusEl.textContent = msg;
statusEl.style.color = 'green';
} catch (err) {
statusEl.textContent = 'Fehler: ' + err.message;
statusEl.style.color = 'red';
}
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
});
// ── 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 === '
')) {
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 el = document.getElementById('footer-status');
el.textContent = message;
el.style.color = color;
el.style.display = 'inline';
setTimeout(() => { el.style.display = 'none'; }, 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 === '
') {
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 loadSyncConfig() {
try {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
const config = result[SYNC_CONFIG_KEY];
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 || '';
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();
}
} catch (_) {}
}
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 el = document.getElementById(elId);
el.textContent = message;
el.style.color = color;
el.style.display = 'inline';
setTimeout(() => { el.style.display = 'none'; }, 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 = '';
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 () => {
showSyncActionStatus('sync-action-status', 'Teste...', '#777');
try {
const result = await browser.runtime.sendMessage({ action: 'testConnection' });
if (result && result.success) {
updateSyncStatus('connected', 'Verbunden');
showSyncActionStatus('sync-action-status', 'Verbindung erfolgreich!', 'green');
appendSyncLog('Verbindungstest erfolgreich.');
loadDepartments();
} else {
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
showSyncActionStatus('sync-action-status', result?.error || 'Fehler', 'red');
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
}
} catch (err) {
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
showSyncActionStatus('sync-action-status', 'Sync nicht verfügbar.', 'red');
}
});
// ── Init ──
window.addEventListener('load', async () => {
await loadSyncHashes();
const templates = await getTemplates();
renderTemplates(templates);
updateTplSyncIndicator();
loadSyncConfig();
loadIdentities();
checkForServerUpdates();
setInterval(checkForServerUpdates, 30000);
});