+
+
+
+
+
+ Sync-Status unbekannt
+
+
+ Vom Server laden
+ Signaturen hochladen
+
+
+
+
+
+
E-Mail Signaturen verwalten
+
Hier kannst du die Signaturen deiner Thunderbird-Identitäten bearbeiten. Änderungen werden direkt in Thunderbird übernommen.
+
+
+ Identität / E-Mail-Adresse
+
+ — Bitte wählen —
+
+
+
+
+
+ Persönliche Signatur verwenden
+
+
+
+
Signatur
+
+
+
+ Signatur speichern
+ Aus Datei laden
+
+
+
+
+
-
Keine Vorlagen vorhanden.
+
+
+
+
+
+
+
Benutzer & Abteilung
+
Dein Name wird bei Änderungen im Commit gespeichert, damit nachvollziehbar ist wer was geändert hat.
+
+
+
+
+
+
+
+
+
Git-Repository Verbindung
+
Verbinde das Plugin mit einem Gitea/Forgejo Repository, um Vorlagen zentral zu verwalten und zwischen Mitarbeitern zu synchronisieren.
+
+
+ Nicht verbunden
+
+
+
+ Server-URL
+
+
+
+
+
+
+
+
+ Verbindung speichern
+ Verbindung testen
+
+
+
+
+
diff --git a/templates_options/templates_options.js b/templates_options/templates_options.js
index 90e794a..d0ea757 100644
--- a/templates_options/templates_options.js
+++ b/templates_options/templates_options.js
@@ -1,253 +1,980 @@
-// templates_options/templates_options.js
-
-const TEMPLATE_STORAGE_KEY = 'message_templates';
-
-// Initialization of IDs
-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');
-
-/**
- * Retrieves all templates from Thunderbird 'storage'
- * @returns {Promise
} Array of template objects
- */
-async function getTemplates() {
- try {
- const result = await browser.storage.local.get(TEMPLATE_STORAGE_KEY);
- // Returns an empty array if no data is stored
- return result[TEMPLATE_STORAGE_KEY] || [];
- } catch (error) {
- console.error("Error retrieving templates:", error);
- return [];
- }
-}
-
-/**
- * Saves the array of templates to Thunderbird 'storage'
- * @param {Array} templates Array of template objects
- */
-async function saveTemplates(templates) {
- try {
- await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
- console.log("Templates successfully saved.");
- } catch (error) {
- console.error("Error saving templates:", error);
- }
-}
-
-
-/**
- * Displays templates on the HTML page
- * @param {Array} templates Array of template objects
- */
-function renderTemplates(templates) {
- templateList.innerHTML = ''; // Clear the existing list
-
- 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';
- item.innerHTML = `
-
- ${template.name}
-
- Bearbeiten
- Löschen
-
- `;
- templateList.appendChild(item);
- });
-
- // Adding Listeners for Edit/Delete
- document.querySelectorAll('.edit-btn').forEach(button => {
- button.addEventListener('click', handleEdit);
- });
- document.querySelectorAll('.delete-btn').forEach(button => {
- button.addEventListener('click', handleDelete);
- });
-}
-
-/**
- * Handles form submission (Add or Save Edited)
- */
-templateForm.addEventListener('submit', async (e) => {
- e.preventDefault();
-
- const id = document.getElementById('template-id').value;
- const name = document.getElementById('template-name').value;
- const content = document.getElementById('template-content').value;
-
- let templates = await getTemplates();
-
- if (id) {
- // EDITING EXISTING
- const index = templates.findIndex(t => t.id === id);
- if (index > -1) {
- templates[index] = { id, name, content };
- }
- } else {
- // ADDING NEW
- const newId = Date.now().toString(); // Simple unique ID
- templates.push({ id: newId, name, content });
- }
-
- await saveTemplates(templates);
- renderTemplates(templates);
- resetForm();
-});
-
-/**
- * Deletes a template
- */
-async function handleDelete(e) {
- if (!confirm('Diese Vorlage wirklich löschen?')) {
- return;
- }
- const idToDelete = e.target.dataset.id;
- let templates = await getTemplates();
-
- // Filter to exclude the template with that ID
- templates = templates.filter(t => t.id !== idToDelete);
-
- await saveTemplates(templates);
- renderTemplates(templates);
-}
-
-/**
- * Loads template data into the edit form
- */
-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;
- document.getElementById('template-content').value = template.content;
-
- // Update UI to signal editing
- formLegend.textContent = 'Vorlage bearbeiten';
- saveButton.textContent = 'Aktualisieren';
- cancelButton.style.display = 'inline';
-
- // Scroll to the top of the form
- window.scrollTo(0, 0);
- }
-}
-
-/**
- * Resets the editing state and clears the form
- */
-function resetForm() {
- templateForm.reset();
- document.getElementById('template-id').value = '';
-
- formLegend.textContent = 'Neue Vorlage erstellen';
- saveButton.textContent = 'Speichern';
- cancelButton.style.display = 'none';
-}
-
-cancelButton.addEventListener('click', resetForm);
-
-/**
- * Select all / deselect all checkboxes
- */
-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);
-});
-
-/**
- * Delete all selected templates
- */
-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;
-
- 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);
-});
-
-/**
- * Handles import of HTML files as templates.
- * Filename (without extension) becomes the template name.
- * Existing templates with the same name are overwritten.
- */
-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, '');
-
- // Extract body content if it's a full HTML document
- let body = content;
- const bodyMatch = content.match(/]*>([\s\S]*)<\/body>/i);
- if (bodyMatch) {
- body = bodyMatch[1].trim();
- }
-
- // Check if template with same name exists
- const existingIndex = templates.findIndex(
- t => t.name.toLowerCase() === name.toLowerCase()
- );
-
- if (existingIndex > -1) {
- // Overwrite existing
- templates[existingIndex].content = body;
- } else {
- // Add new
- templates.push({
- id: Date.now().toString() + importCount,
- name: name,
- content: body
- });
- }
- importCount++;
- }
-
- await saveTemplates(templates);
- renderTemplates(templates);
-
- statusEl.textContent = `${importCount} Vorlage(n) importiert!`;
- statusEl.style.color = 'green';
- statusEl.style.display = 'inline';
- fileInput.value = '';
-
- setTimeout(() => { statusEl.style.display = 'none'; }, 3000);
-});
-
-// Load templates on page load
-window.addEventListener('load', async () => {
- const templates = await getTemplates();
- renderTemplates(templates);
-});
\ No newline at end of file
+// templates_options/templates_options.js
+
+const TEMPLATE_STORAGE_KEY = 'message_templates';
+
+// DOM elements
+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');
+
+// ── 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'
+];
+
+// Build list of available system fonts
+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(); // prevent blur before click fires
+ 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', () => {
+ // Small delay so mousedown on option fires first
+ setTimeout(() => fontDropdown.classList.remove('open'), 150);
+});
+
+// Apply custom font on Enter
+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);
+ }
+});
+
+// ── Contenteditable Editor ──
+
+function getEditorContent() {
+ const html = editorArea.innerHTML;
+ return (!html || html === ' ') ? '' : html;
+}
+
+function setEditorContent(html) {
+ editorArea.innerHTML = html || '';
+}
+
+// ── Toolbar Commands ──
+
+document.querySelectorAll('.editor-toolbar button[data-cmd]').forEach(btn => {
+ btn.addEventListener('click', () => {
+ editorArea.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', () => {
+ editorArea.focus();
+ document.execCommand(cmd, false, colorInput.value);
+ });
+ colorInput.click();
+ return;
+ }
+ }
+
+ document.execCommand(cmd, false, val);
+ });
+});
+
+// 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 = '';
+});
+
+// ── 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');
+ });
+});
+
+// ── 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) {
+ try {
+ await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
+ } catch (error) {
+ console.error("Error saving templates:", error);
+ }
+}
+
+// ── 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';
+
+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 || {};
+ } catch (_) {}
+}
+
+async function saveSyncHashes() {
+ await browser.storage.local.set({
+ [HASH_STORAGE_KEY]: { tpl: tplSyncedHashes, sig: sigSyncedHashes }
+ });
+}
+
+function storeTplHashes(templates) {
+ tplSyncedHashes = {};
+ for (const t of templates) {
+ tplSyncedHashes[t.id] = simpleHash(t.content || '');
+ }
+ 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() {
+ // Update the global indicator based on all templates
+ 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 = template.folder
+ ? `↓ `
+ : '';
+ item.innerHTML = `
+
+
+ ${template.name}${folderBadge}
+
+ ${pullBtn}${pushBtn}
+ Bearbeiten
+ Löschen
+
+ `;
+ 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);
+ });
+}
+
+// ── 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();
+
+ 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 };
+ }
+ } else {
+ const newId = Date.now().toString();
+ templates.push({ id: newId, name, content });
+ }
+
+ await saveTemplates(templates);
+ renderTemplates(templates);
+ resetForm();
+ updateTplSyncIndicator();
+});
+
+// ── Edit / Delete ──
+
+async function handleDelete(e) {
+ if (!confirm('Diese Vorlage wirklich löschen?')) return;
+ const idToDelete = e.target.dataset.id;
+ let templates = await getTemplates();
+ templates = templates.filter(t => t.id !== idToDelete);
+ 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) {
+ // Update local template with pulled content
+ template.content = result.content;
+ await saveTemplates(templates);
+ tplSyncedHashes[id] = simpleHash(result.content || '');
+ 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;
+ }
+}
+
+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', () => {
+ 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} Template(s) 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: 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);
+});
+
+// ── 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 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 loadIdentities() {
+ allIdentities = [];
+ sigIdentitySelect.innerHTML = '— Bitte wählen — ';
+
+ 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: 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 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);
+ } else {
+ sigEditorArea.innerHTML = '';
+ sigPersonalToggle.checked = false;
+ sigPersonalInfo.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 () => {
+ const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
+ if (!identity) return;
+ await setPersonalEmail(identity.email, sigPersonalToggle.checked);
+ updatePersonalInfo(identity.email, sigPersonalToggle.checked);
+});
+
+// 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;
+
+ 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;
+ }
+ }
+ document.execCommand(cmd, false, val);
+ });
+});
+
+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
+document.getElementById('sig-save-button').addEventListener('click', async () => {
+ const identityId = sigIdentitySelect.value;
+ if (!identityId) {
+ showSigStatus('Bitte Identität auswählen.', 'red');
+ return;
+ }
+
+ const html = sigEditorArea.innerHTML;
+ await browser.identities.update(identityId, {
+ signature: html,
+ signatureIsPlainText: false
+ });
+
+ // Update local cache
+ const identity = allIdentities.find(i => i.id === identityId);
+ if (identity) identity.signature = html;
+ updateSigSyncIndicator();
+
+ showSigStatus('Signatur gespeichert!', 'green');
+});
+
+// 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 file = e.target.files[0];
+ if (!file) return;
+
+ const content = await file.text();
+ let body = content;
+ const bodyMatch = content.match(/]*>([\s\S]*)<\/body>/i);
+ if (bodyMatch) body = bodyMatch[1].trim();
+
+ sigEditorArea.innerHTML = body;
+ e.target.value = '';
+ showSigStatus('Datei geladen — jetzt "Signatur speichern" klicken.', '#555');
+});
+
+// Signature sync - pull
+document.getElementById('sig-sync-pull').addEventListener('click', async () => {
+ const statusEl = document.getElementById('sig-sync-status');
+ statusEl.textContent = 'Lade...';
+ 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';
+ statusEl.style.color = 'red';
+ }
+ } catch (err) {
+ statusEl.textContent = 'Fehler: ' + err.message;
+ statusEl.style.color = 'red';
+ }
+ 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';
+
+ 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 || '');
+ }
+ saveSyncHashes();
+ updateSigSyncIndicator();
+ } else {
+ statusEl.textContent = result?.error || 'Fehler';
+ statusEl.style.color = 'red';
+ }
+ } catch (err) {
+ statusEl.textContent = 'Fehler: ' + err.message;
+ statusEl.style.color = '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);
+ 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');
+ }
+ // 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 (_) {}
+}
+
+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');
+ const currentVal = 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;
+ 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) {
+ 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.');
+
+ // 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 {
+ 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.`);
+ }
+ } 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);
+ 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 });
+ });
+}
+
+// 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 {
+ 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');
+ }
+});
+
+// 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 () => {
+ await loadSyncHashes();
+ const templates = await getTemplates();
+ renderTemplates(templates);
+ updateTplSyncIndicator();
+ loadSyncConfig();
+ loadIdentities();
+});