Nicht verbunden
diff --git a/templates_options/templates_options.js b/templates_options/templates_options.js
index d0ea757..79238ea 100644
--- a/templates_options/templates_options.js
+++ b/templates_options/templates_options.js
@@ -1,8 +1,14 @@
// 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
+// ── 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');
@@ -11,6 +17,11 @@ 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 = [
@@ -27,7 +38,6 @@ const FONT_CANDIDATES = [
'Ubuntu', 'Verdana'
];
-// Build list of available system fonts
const availableFonts = FONT_CANDIDATES.filter(f => document.fonts.check(`12px "${f}"`));
const fontInput = document.getElementById('font-input');
@@ -44,7 +54,7 @@ function renderFontDropdown(filter) {
div.textContent = font;
div.style.fontFamily = font;
div.addEventListener('mousedown', (e) => {
- e.preventDefault(); // prevent blur before click fires
+ e.preventDefault();
fontInput.value = '';
fontDropdown.classList.remove('open');
editorArea.focus();
@@ -52,18 +62,12 @@ function renderFontDropdown(filter) {
});
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', () => {
- // Small delay so mousedown on option fires first
- setTimeout(() => fontDropdown.classList.remove('open'), 150);
-});
-
-// Apply custom font on Enter
+fontInput.addEventListener('blur', () => setTimeout(() => fontDropdown.classList.remove('open'), 150));
fontInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
@@ -76,7 +80,7 @@ fontInput.addEventListener('keydown', (e) => {
}
});
-// ── Contenteditable Editor ──
+// ── Editor Helpers ──
function getEditorContent() {
const html = editorArea.innerHTML;
@@ -87,43 +91,112 @@ function setEditorContent(html) {
editorArea.innerHTML = html || '';
}
-// ── Toolbar Commands ──
+// ── Toolbar Commands (template editor) ──
-document.querySelectorAll('.editor-toolbar button[data-cmd]').forEach(btn => {
- btn.addEventListener('click', () => {
- editorArea.focus();
+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;
- 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', () => {
- editorArea.focus();
- document.execCommand(cmd, false, colorInput.value);
- });
- colorInput.click();
- return;
+ 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);
+ 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);
+ }
});
-// Font size dropdown
-document.querySelector('.editor-toolbar select[data-cmd="fontSize"]').addEventListener('change', function() {
- if (!this.value) return;
- editorArea.focus();
- document.execCommand('fontSize', false, this.value);
- this.value = '';
-});
+// ── 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 ──
@@ -136,6 +209,13 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
});
});
+// ── 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() {
@@ -149,19 +229,16 @@ async function getTemplates() {
}
async function saveTemplates(templates) {
- try {
- await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
- } catch (error) {
- console.error("Error saving templates:", error);
- }
+ await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
}
// ── Sync Status Tracking ──
-let tplSyncedHashes = {}; // template id -> hash of content after last pull/push
-let sigSyncedHashes = {}; // email -> hash of signature after last pull/push
-
-const HASH_STORAGE_KEY = 'sync_hashes';
+let tplSyncedHashes = {};
+let sigSyncedHashes = {};
+let lastPulledShas = {};
+let currentRemoteShas = {};
+let allIdentities = [];
function simpleHash(str) {
let h = 0;
@@ -177,20 +254,43 @@ async function loadSyncHashes() {
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 }
+ [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();
}
@@ -200,7 +300,6 @@ function getTplSyncClass(template) {
}
function updateTplSyncIndicator() {
- // Update the global indicator based on all templates
const el = document.getElementById('tpl-sync-indicator');
if (!el) return;
const dot = el.querySelector('.sync-dot');
@@ -281,11 +380,11 @@ function renderTemplates(templates) {
const pushBtn = syncClass === 'out-of-sync'
? `
@@ -297,25 +396,42 @@ function renderTemplates(templates) {
templateList.appendChild(item);
});
- document.querySelectorAll('.edit-btn').forEach(button => {
- button.addEventListener('click', handleEdit);
- });
- document.querySelectorAll('.delete-btn').forEach(button => {
- button.addEventListener('click', handleDelete);
- });
- document.querySelectorAll('.push-btn').forEach(button => {
- button.addEventListener('click', handlePushSingle);
- });
- document.querySelectorAll('.pull-btn').forEach(button => {
- button.addEventListener('click', handlePullSingle);
- });
+ 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();
@@ -327,32 +443,74 @@ templateForm.addEventListener('submit', async (e) => {
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) {
- // Preserve folder info from synced templates
- const folder = templates[index].folder;
const remotePath = templates[index].remotePath;
- templates[index] = { id, name, content, folder, remotePath };
+ const effectiveFolder = folder !== undefined ? folder : templates[index].folder;
+ templates[index] = { id, name, content, folder: effectiveFolder, remotePath };
}
} else {
- const newId = Date.now().toString();
- templates.push({ id: newId, name, content });
+ templates.push({ id: Date.now().toString(), name, content, folder: folder || undefined });
}
await saveTemplates(templates);
renderTemplates(templates);
- resetForm();
+ closeEditorPanel();
updateTplSyncIndicator();
});
-// ── Edit / Delete ──
+// ── 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) {
- if (!confirm('Diese Vorlage wirklich löschen?')) return;
- const idToDelete = e.target.dataset.id;
+ const id = e.target.dataset.id;
let templates = await getTemplates();
- templates = templates.filter(t => t.id !== idToDelete);
+ 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();
@@ -373,10 +531,12 @@ async function handlePullSingle(e) {
remotePath: template.remotePath
});
if (result && result.success) {
- // Update local template with pulled content
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();
@@ -423,36 +583,6 @@ async function handlePushSingle(e) {
}
}
-async function handleEdit(e) {
- const idToEdit = e.target.dataset.id;
- const templates = await getTemplates();
- const template = templates.find(t => t.id === idToEdit);
-
- if (template) {
- document.getElementById('template-id').value = template.id;
- document.getElementById('template-name').value = template.name;
- setEditorContent(template.content);
-
- formLegend.textContent = 'Vorlage bearbeiten';
- saveButton.textContent = 'Aktualisieren';
- cancelButton.style.display = 'inline';
-
- window.scrollTo(0, 0);
- }
-}
-
-function resetForm() {
- templateForm.reset();
- document.getElementById('template-id').value = '';
- setEditorContent('');
-
- formLegend.textContent = 'Neue Vorlage erstellen';
- saveButton.textContent = 'Speichern';
- cancelButton.style.display = 'none';
-}
-
-cancelButton.addEventListener('click', resetForm);
-
// ── Bulk Actions ──
document.getElementById('select-all-button').addEventListener('click', () => {
@@ -464,7 +594,7 @@ document.getElementById('select-all-button').addEventListener('click', () => {
document.getElementById('delete-selected-button').addEventListener('click', async () => {
const checked = document.querySelectorAll('.template-checkbox:checked');
if (checked.length === 0) return;
- if (!confirm(`${checked.length} Template(s) wirklich löschen?`)) 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();
@@ -493,25 +623,15 @@ document.getElementById('import-button').addEventListener('click', async () => {
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 (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: name,
- content: body
- });
+ templates.push({ id: Date.now().toString() + importCount, name, content: body });
}
importCount++;
}
@@ -524,32 +644,91 @@ document.getElementById('import-button').addEventListener('click', async () => {
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 ──
-const sigIdentitySelect = document.getElementById('sig-identity-select');
-const sigEditorArea = document.getElementById('sig-editor-area');
-const sigPersonalToggle = document.getElementById('sig-personal-toggle');
-const sigPersonalInfo = document.getElementById('sig-personal-info');
-let allIdentities = [];
-
-// Persistent setting: which identities use personal signatures
-const SIG_PERSONAL_KEY = 'sig_personal_emails';
-
-async function getPersonalEmails() {
- const result = await browser.storage.local.get(SIG_PERSONAL_KEY);
- return result[SIG_PERSONAL_KEY] || [];
+async function getSigSourceMap() {
+ const result = await browser.storage.local.get(SIG_SOURCE_KEY);
+ return result[SIG_SOURCE_KEY] || {};
}
-async function setPersonalEmail(email, enabled) {
- const list = await getPersonalEmails();
- const set = new Set(list);
- if (enabled) set.add(email.toLowerCase());
- else set.delete(email.toLowerCase());
- await browser.storage.local.set({ [SIG_PERSONAL_KEY]: [...set] });
+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() {
@@ -566,7 +745,7 @@ async function loadIdentities() {
allIdentities.push({
id: identity.id,
email: identity.email,
- label: label,
+ label,
accountName: account.name,
signature: identity.signature || '',
signatureIsPlainText: identity.signatureIsPlainText || false
@@ -579,66 +758,116 @@ async function loadIdentities() {
}
}
-// Load signature into editor when identity is selected
+// Load signature header into editor when identity is selected
sigIdentitySelect.addEventListener('change', async () => {
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
if (identity) {
- sigEditorArea.innerHTML = identity.signature || '';
- const personalEmails = await getPersonalEmails();
- const isPersonal = personalEmails.includes(identity.email.toLowerCase());
- sigPersonalToggle.checked = isPersonal;
- updatePersonalInfo(identity.email, isPersonal);
+ 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 = '';
- sigPersonalToggle.checked = false;
- sigPersonalInfo.textContent = '';
+ sigSourceSelect.innerHTML = '
Eigene Signatur ';
+ sigSourceInfo.textContent = '';
}
});
-function updatePersonalInfo(email, isPersonal) {
- const configName = document.getElementById('sync-author-name')?.value || '';
- if (isPersonal && configName) {
- sigPersonalInfo.textContent = `(${email} → persönlich für ${configName})`;
- } else if (isPersonal) {
- sigPersonalInfo.textContent = '(Name in Sync-Einstellungen eintragen!)';
- } else {
- sigPersonalInfo.textContent = '(gemeinsame Signatur für alle)';
- }
-}
-
-// Toggle personal signature
-sigPersonalToggle.addEventListener('change', async () => {
+sigSourceSelect.addEventListener('change', async () => {
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
if (!identity) return;
- await setPersonalEmail(identity.email, sigPersonalToggle.checked);
- updatePersonalInfo(identity.email, sigPersonalToggle.checked);
+
+ 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 toolbar commands
-document.querySelectorAll('#sig-toolbar button[data-cmd]').forEach(btn => {
- btn.addEventListener('click', () => {
- sigEditorArea.focus();
- const cmd = btn.dataset.cmd;
- let val = btn.dataset.val || null;
+// ── Signature Header/Footer Helpers ──
- 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', () => {
- sigEditorArea.focus();
- document.execCommand(cmd, false, colorInput.value);
- });
- colorInput.click();
- return;
- }
+// 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;
}
- document.execCommand(cmd, false, val);
- });
+ } 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) {
@@ -649,7 +878,7 @@ function showSigStatus(message, color) {
setTimeout(() => { el.style.display = 'none'; }, 4000);
}
-// Save signature to Thunderbird identity
+// Save signature to Thunderbird identity (header + footer)
document.getElementById('sig-save-button').addEventListener('click', async () => {
const identityId = sigIdentitySelect.value;
if (!identityId) {
@@ -657,18 +886,46 @@ document.getElementById('sig-save-button').addEventListener('click', async () =>
return;
}
- const html = sigEditorArea.innerHTML;
+ 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: html,
+ signature: fullSignature,
signatureIsPlainText: false
});
- // Update local cache
- const identity = allIdentities.find(i => i.id === identityId);
- if (identity) identity.signature = html;
+ if (identity) identity.signature = fullSignature;
updateSigSyncIndicator();
- showSigStatus('Signatur gespeichert!', 'green');
+ // 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
@@ -677,42 +934,80 @@ document.getElementById('sig-import-file-btn').addEventListener('click', () => {
});
document.getElementById('sig-import-file').addEventListener('change', async (e) => {
- const file = e.target.files[0];
- if (!file) return;
+ const files = Array.from(e.target.files);
+ if (files.length === 0) return;
- const content = await file.text();
- let body = content;
- const bodyMatch = content.match(/]*>([\s\S]*)<\/body>/i);
- if (bodyMatch) body = bodyMatch[1].trim();
+ const htmlFile = files.find(f => /\.html?$/i.test(f.name));
+ const imageFiles = files.filter(f => f.type.startsWith('image/'));
- sigEditorArea.innerHTML = body;
+ 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 = '';
- showSigStatus('Datei geladen — jetzt "Signatur speichern" klicken.', '#555');
+
+ 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 - pull
-document.getElementById('sig-sync-pull').addEventListener('click', async () => {
+// Signature sync - "Aktualisieren" (pull + push)
+document.getElementById('sig-sync-refresh').addEventListener('click', async () => {
const statusEl = document.getElementById('sig-sync-status');
- statusEl.textContent = 'Lade...';
+ statusEl.textContent = 'Synchronisiere...';
statusEl.style.color = '#777';
statusEl.style.display = 'inline';
try {
- const result = await browser.runtime.sendMessage({ action: 'pullSignatures' });
- if (result && result.success) {
- statusEl.textContent = `${result.updated || 0} Signatur(en) geladen!`;
- statusEl.style.color = 'green';
- await loadIdentities();
- for (const id of allIdentities) {
- sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
- }
- saveSyncHashes();
- updateSigSyncIndicator();
- sigIdentitySelect.dispatchEvent(new Event('change'));
- } else {
- statusEl.textContent = result?.error || 'Fehler';
+ // 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';
@@ -720,38 +1015,75 @@ document.getElementById('sig-sync-pull').addEventListener('click', async () => {
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
});
-// Signature sync - push
-document.getElementById('sig-sync-push').addEventListener('click', async () => {
- const statusEl = document.getElementById('sig-sync-status');
- statusEl.textContent = 'Lade hoch...';
- statusEl.style.color = '#777';
- statusEl.style.display = 'inline';
+// ── Footer Editor ──
- try {
- const result = await browser.runtime.sendMessage({ action: 'pushSignatures' });
- if (result && result.success) {
- statusEl.textContent = `${result.pushed || 0} Signatur(en) hochgeladen!`;
- statusEl.style.color = 'green';
- for (const id of allIdentities) {
- sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
+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;
}
- saveSyncHashes();
- updateSigSyncIndicator();
+ } 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 {
- statusEl.textContent = result?.error || 'Fehler';
- statusEl.style.color = 'red';
+ showFooterStatus(result?.error || 'Fehler', 'red');
}
} catch (err) {
- statusEl.textContent = 'Fehler: ' + err.message;
- statusEl.style.color = 'red';
+ 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');
}
- setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
});
// ── Sync Settings UI ──
-const SYNC_CONFIG_KEY = 'gitea_config';
-
async function loadSyncConfig() {
try {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
@@ -767,17 +1099,14 @@ async function loadSyncConfig() {
if (config.baseUrl && config.token) {
updateSyncStatus('connected', 'Verbunden');
}
- // Load department after a short delay to ensure config is available
if (config.department) {
const deptSelect = document.getElementById('sync-department');
- // Add saved department as option in case list hasn't loaded yet
const opt = document.createElement('option');
opt.value = config.department;
opt.textContent = config.department;
opt.selected = true;
deptSelect.appendChild(opt);
}
- // Try to load department list
loadDepartments();
}
} catch (_) {}
@@ -823,20 +1152,21 @@ async function loadDepartments() {
const result = await browser.runtime.sendMessage({ action: 'listDepartments' });
if (result && result.success) {
const select = document.getElementById('sync-department');
- const currentVal = select.value;
+ // 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 = '
— Bitte wählen — ';
for (const dept of result.departments) {
const opt = document.createElement('option');
opt.value = dept;
opt.textContent = dept;
- if (dept === currentVal) opt.selected = true;
+ if (dept === savedDept) opt.selected = true;
select.appendChild(opt);
}
}
} catch (_) {}
}
-// Save sync config (connection + user info)
document.getElementById('save-sync-config').addEventListener('click', async () => {
const config = getSyncConfigFromForm();
if (!config.baseUrl || !config.owner || !config.repo || !config.token) {
@@ -860,21 +1190,18 @@ document.getElementById('save-sync-config').addEventListener('click', async () =
updateSyncStatus('connected', 'Verbunden');
showSyncActionStatus('sync-action-status', 'Gespeichert!', 'green');
appendSyncLog('Verbindung konfiguriert.');
-
- // Load departments after saving
loadDepartments();
});
-// Save department selection immediately when changed
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 });
- // Auto-pull templates for the new department
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();
@@ -883,11 +1210,15 @@ document.getElementById('sync-department').addEventListener('change', async () =
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 (_) {}
}
});
-// Save author name/email immediately when changed
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);
@@ -898,10 +1229,8 @@ for (const id of ['sync-author-name', 'sync-author-email']) {
});
}
-// Refresh departments button
document.getElementById('refresh-departments').addEventListener('click', loadDepartments);
-// Test connection
document.getElementById('test-sync-connection').addEventListener('click', async () => {
showSyncActionStatus('sync-action-status', 'Teste...', '#777');
try {
@@ -922,52 +1251,6 @@ document.getElementById('test-sync-connection').addEventListener('click', async
}
});
-// Pull (Vorlagen vom Server laden)
-document.getElementById('sync-pull-button').addEventListener('click', async () => {
- showSyncActionStatus('sync-sync-status', 'Lade Vorlagen...', '#777');
- appendSyncLog('Vorlagen werden vom Server geladen...');
- try {
- const result = await browser.runtime.sendMessage({ action: 'pullTemplates' });
- if (result && result.success) {
- showSyncActionStatus('sync-sync-status', `${result.updated || 0} Vorlage(n) geladen!`, 'green');
- appendSyncLog(`Pull abgeschlossen: ${result.updated || 0} Vorlage(n).`);
- const templates = await getTemplates();
- renderTemplates(templates);
- storeTplHashes(templates);
- updateTplSyncIndicator();
- } else {
- showSyncActionStatus('sync-sync-status', result?.error || 'Fehler', 'red');
- appendSyncLog('Pull fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
- }
- } catch (err) {
- showSyncActionStatus('sync-sync-status', 'Fehler: ' + err.message, 'red');
- appendSyncLog('Fehler: ' + err.message);
- }
-});
-
-// Push (Änderungen hochladen — nur per Knopfdruck)
-document.getElementById('sync-push-button').addEventListener('click', async () => {
- showSyncActionStatus('sync-sync-status', 'Lade hoch...', '#777');
- appendSyncLog('Änderungen werden hochgeladen...');
- try {
- const result = await browser.runtime.sendMessage({ action: 'pushTemplates' });
- if (result && result.success) {
- showSyncActionStatus('sync-sync-status', `${result.pushed || 0} Änderung(en) hochgeladen!`, 'green');
- appendSyncLog(`Push abgeschlossen: ${result.pushed || 0} Änderung(en).`);
- const templates = await getTemplates();
- renderTemplates(templates);
- storeTplHashes(templates);
- updateTplSyncIndicator();
- } else {
- showSyncActionStatus('sync-sync-status', result?.error || 'Fehler', 'red');
- appendSyncLog('Push fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
- }
- } catch (err) {
- showSyncActionStatus('sync-sync-status', 'Fehler: ' + err.message, 'red');
- appendSyncLog('Fehler: ' + err.message);
- }
-});
-
// ── Init ──
window.addEventListener('load', async () => {
@@ -977,4 +1260,7 @@ window.addEventListener('load', async () => {
updateTplSyncIndicator();
loadSyncConfig();
loadIdentities();
+
+ checkForServerUpdates();
+ setInterval(checkForServerUpdates, 30000);
});