diff --git a/lib/gitea-sync.js b/lib/gitea-sync.js index e006069..fdcc2ba 100644 --- a/lib/gitea-sync.js +++ b/lib/gitea-sync.js @@ -46,7 +46,8 @@ class GiteaClient { } static fromBase64(b64) { - return decodeURIComponent(escape(atob(b64))); + if (!b64) return ''; + return decodeURIComponent(escape(atob(b64.replace(/\s/g, '')))); } encodePath(p) { @@ -206,6 +207,28 @@ class SyncManager { return { success: true, departments }; } + /** + * Lightweight check: get remote file SHAs without downloading content + */ + async checkRemoteShas() { + if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); + if (!this.department) throw new Error('Keine Abteilung ausgewählt'); + + const remoteShas = {}; // "folder/filename" -> sha + const folders = [SHARED_FOLDER, this.department]; + + for (const folder of folders) { + const files = await this.client.listDir(folder); + for (const file of files) { + if (file.name.endsWith('.html')) { + remoteShas[`${folder}/${file.name}`] = file.sha; + } + } + } + + return { success: true, remoteShas }; + } + /** * Pull templates from repo: department folder + shared folder */ @@ -328,93 +351,203 @@ class SyncManager { return { success: true, pushed }; } - /** - * Pull signatures from repo signatures/ folder and apply to matching Thunderbird identities - * Filename = email address (e.g. info@hotel.de.html) - */ - /** - * Get the personal filename slug from author name - */ get authorSlug() { return SyncManager.toFilename(this.config.authorName || ''); } + static get FOOTER_SEPARATOR() { + return ''; + } + + static extractHeader(fullSignature) { + const idx = fullSignature.indexOf(SyncManager.FOOTER_SEPARATOR); + if (idx === -1) return fullSignature; + return fullSignature.substring(0, idx).trim(); + } + + static combineSignature(header, footer) { + if (!footer) return header; + return header + '\n' + SyncManager.FOOTER_SEPARATOR + '\n' + footer; + } + /** - * Pull signatures from repo. - * For each identity: if personal sig enabled AND personal file exists → use it. - * Otherwise use the shared file (email.html). + * Load the signature header template from signatures/headers/_vorlage.html + */ + async loadSignatureTemplate() { + if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); + + const fileData = await this.client.getFile('signatures/headers/_vorlage.html'); + if (!fileData || !fileData.content) { + throw new Error('Vorlage nicht gefunden unter signatures/headers/_vorlage.html'); + } + + const html = GiteaClient.fromBase64(fileData.content); + return { success: true, html }; + } + + /** + * Pull footer for current department. + * Tries signatures/footers/{department}.html first, falls back to signatures/footers/_default.html + */ + async pullFooter() { + let fileData = null; + + // Try department-specific footer first + if (this.department) { + fileData = await this.client.getFile(`signatures/footers/${this.department}.html`); + } + + // Fall back to default + if (!fileData || !fileData.content) { + fileData = await this.client.getFile('signatures/footers/_default.html'); + } + + // Legacy fallback: old single-file location + if (!fileData || !fileData.content) { + fileData = await this.client.getFile('signatures/_footer.html'); + } + + if (!fileData || !fileData.content) return ''; + + const footer = GiteaClient.fromBase64(fileData.content); + await browser.storage.local.set({ 'sig_footer_cache': footer }); + return footer; + } + + /** + * Load footer for editing (returns HTML) + */ + async loadFooter() { + if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); + + const footer = await this.pullFooter(); + return { success: true, html: footer }; + } + + /** + * Push footer for current department to signatures/footers/{department}.html + */ + async pushFooter(html) { + if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); + if (!this.config.authorName) throw new Error('Bitte Name eintragen'); + if (!this.department) throw new Error('Keine Abteilung ausgewählt'); + + const filepath = `signatures/footers/${this.department}.html`; + const commitMsg = `Signatur-Footer ${this.department} - von ${this.config.authorName}`; + + const existing = await this.client.getFile(filepath); + + if (existing && existing.content) { + const existingContent = GiteaClient.fromBase64(existing.content); + if (existingContent !== html) { + await this.client.updateFile(filepath, html, existing.sha, commitMsg); + } + } else { + await this.client.createFile(filepath, html, commitMsg); + } + + await browser.storage.local.set({ 'sig_footer_cache': html }); + return { success: true }; + } + + /** + * Pull signatures from repo using header/footer baustein model. + * - Loads shared footer from signatures/_footer.html + * - Loads personal headers from signatures/headers/email.authorslug.html + * - Combines header + footer and applies to Thunderbird identity + * - Resolves "=other@" references in second pass */ async pullSignatures() { if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); - const files = await this.client.listDir('signatures'); - if (files.length === 0) { - return { success: true, updated: 0 }; - } + // Pull shared footer + const footer = await this.pullFooter(); - // Build lookup: filename → file entry - const fileMap = {}; - for (const f of files) { - if (f.name.endsWith('.html')) { - fileMap[f.name.toLowerCase()] = f; + // List header files + const headerFiles = await this.client.listDir('signatures/headers'); + const headerMap = {}; + for (const f of headerFiles) { + if (f.name.endsWith('.html') && !f.name.startsWith('_')) { + headerMap[f.name.toLowerCase()] = f; } } - // Get personal email list - const personalResult = await browser.storage.local.get('sig_personal_emails'); - const personalEmails = new Set((personalResult.sig_personal_emails || []).map(e => e.toLowerCase())); + // Get source map + const sourceResult = await browser.storage.local.get('sig_source_map'); + const sourceMap = sourceResult.sig_source_map || {}; - // Get all Thunderbird identities const accounts = await browser.accounts.list(); let updated = 0; + const loadedHeaders = {}; // email → header html + const allIdentityList = []; + // First pass: load headers for "own" identities for (const account of accounts) { const identities = await browser.identities.list(account.id); for (const identity of identities) { const email = identity.email.toLowerCase(); - const isPersonal = personalEmails.has(email); + const source = sourceMap[email] || 'own'; + allIdentityList.push({ id: identity.id, email }); + if (source.startsWith('=')) continue; + + // Try personal header: email.authorslug.html let targetFile = null; - - if (isPersonal && this.authorSlug) { - // Try personal file first: email.authorslug.html + if (this.authorSlug) { const personalName = `${email}.${this.authorSlug}.html`; - targetFile = fileMap[personalName] || null; + targetFile = headerMap[personalName] || null; } - // Fall back to shared file: email.html - if (!targetFile) { - targetFile = fileMap[`${email}.html`] || null; - } - - if (!targetFile) continue; + if (!targetFile) continue; // No header file yet — user hasn't set up signature const fileData = await this.client.getFile(targetFile.path); if (!fileData) continue; - const signature = GiteaClient.fromBase64(fileData.content); + const header = GiteaClient.fromBase64(fileData.content); + loadedHeaders[email] = header; + const fullSig = SyncManager.combineSignature(header, footer); + await browser.identities.update(identity.id, { - signature: signature, + signature: fullSig, signatureIsPlainText: false }); updated++; } } + // Second pass: resolve "=other@" references + for (const { id, email } of allIdentityList) { + const source = sourceMap[email] || 'own'; + if (!source.startsWith('=')) continue; + + const srcEmail = source.substring(1).toLowerCase(); + const srcHeader = loadedHeaders[srcEmail]; + if (srcHeader !== undefined) { + const fullSig = SyncManager.combineSignature(srcHeader, footer); + await browser.identities.update(id, { + signature: fullSig, + signatureIsPlainText: false + }); + loadedHeaders[email] = srcHeader; + updated++; + } + } + return { success: true, updated }; } /** - * Push signatures to repo. - * Personal: saves as email.authorslug.html - * Shared: saves as email.html + * Push signature headers to repo. + * Only the header part is pushed as signatures/headers/email.authorslug.html + * Skips "=other@" identities. */ async pushSignatures() { if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)'); + if (!this.authorSlug) throw new Error('Name fehlt für Dateiname'); - const personalResult = await browser.storage.local.get('sig_personal_emails'); - const personalEmails = new Set((personalResult.sig_personal_emails || []).map(e => e.toLowerCase())); + const sourceResult = await browser.storage.local.get('sig_source_map'); + const sourceMap = sourceResult.sig_source_map || {}; const accounts = await browser.accounts.list(); let pushed = 0; @@ -425,29 +558,27 @@ class SyncManager { if (!identity.signature) continue; const email = identity.email.toLowerCase(); - const isPersonal = personalEmails.has(email); + const source = sourceMap[email] || 'own'; + if (source.startsWith('=')) continue; - let filename; - if (isPersonal && this.authorSlug) { - filename = `${email}.${this.authorSlug}.html`; - } else { - filename = `${email}.html`; - } + // Extract just the header from the full signature + const header = SyncManager.extractHeader(identity.signature); + if (!header.trim()) continue; - const filepath = `signatures/${filename}`; - const label = isPersonal ? `(persönlich)` : `(gemeinsam)`; - const commitMsg = `Signatur ${identity.email} ${label} - von ${this.config.authorName}`; + const filename = `${email}.${this.authorSlug}.html`; + const filepath = `signatures/headers/${filename}`; + const commitMsg = `Signatur-Header ${identity.email} - von ${this.config.authorName}`; const existing = await this.client.getFile(filepath); if (existing) { const existingContent = GiteaClient.fromBase64(existing.content); - if (existingContent !== identity.signature) { - await this.client.updateFile(filepath, identity.signature, existing.sha, commitMsg); + if (existingContent !== header) { + await this.client.updateFile(filepath, header, existing.sha, commitMsg); pushed++; } } else { - await this.client.createFile(filepath, identity.signature, commitMsg); + await this.client.createFile(filepath, header, commitMsg); pushed++; } } @@ -501,6 +632,22 @@ class SyncManager { return { success: true }; } + /** + * Delete a template from the remote repo by its remote path + */ + async deleteRemoteTemplate(remotePath) { + if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); + if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)'); + + const fileData = await this.client.getFile(remotePath); + if (!fileData) throw new Error('Datei nicht im Repository gefunden'); + + const commitMsg = `${remotePath.split('/').pop()} gelöscht von ${this.config.authorName}`; + await this.client.deleteFile(remotePath, fileData.sha, commitMsg); + + return { success: true }; + } + async testConnection() { if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); const repoInfo = await this.client.testConnection(); @@ -515,7 +662,7 @@ const syncManager = new SyncManager(); // ── Background message handler for sync ── browser.runtime.onMessage.addListener(async (msg, sender) => { - const syncActions = ['testConnection', 'pullTemplates', 'pushTemplates', 'pullSingleTemplate', 'pushSingleTemplate', 'listDepartments', 'pullSignatures', 'pushSignatures']; + const syncActions = ['testConnection', 'pullTemplates', 'pushTemplates', 'pullSingleTemplate', 'pushSingleTemplate', 'deleteRemoteTemplate', 'listDepartments', 'checkRemoteShas', 'pullSignatures', 'pushSignatures', 'loadSignatureTemplate', 'loadFooter', 'pushFooter']; if (!syncActions.includes(msg.action)) return; try { @@ -533,6 +680,9 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { case 'listDepartments': return await syncManager.listDepartments(); + case 'checkRemoteShas': + return await syncManager.checkRemoteShas(); + case 'pullTemplates': return await syncManager.pullTemplates(); @@ -545,12 +695,24 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { case 'pushSingleTemplate': return await syncManager.pushSingleTemplate(msg.templateId); + case 'deleteRemoteTemplate': + return await syncManager.deleteRemoteTemplate(msg.remotePath); + case 'pullSignatures': return await syncManager.pullSignatures(); case 'pushSignatures': return await syncManager.pushSignatures(); + case 'loadSignatureTemplate': + return await syncManager.loadSignatureTemplate(); + + case 'loadFooter': + return await syncManager.loadFooter(); + + case 'pushFooter': + return await syncManager.pushFooter(msg.html); + default: return { success: false, error: 'Unbekannte Aktion' }; } diff --git a/lib/mdi/mdi-editor.css b/lib/mdi/mdi-editor.css index 4a11bf5..5bc53dd 100644 --- a/lib/mdi/mdi-editor.css +++ b/lib/mdi/mdi-editor.css @@ -26,3 +26,4 @@ .mdi-format-align-right::before { content: "\F0263"; } .mdi-link::before { content: "\F0337"; } .mdi-format-clear::before { content: "\F0265"; } +.mdi-image::before { content: "\F02E9"; } diff --git a/templates-reply-hotel.xpi b/templates-reply-hotel.xpi index 8bf1a4f..a9ed110 100644 Binary files a/templates-reply-hotel.xpi and b/templates-reply-hotel.xpi differ diff --git a/templates_options/templates_options.html b/templates_options/templates_options.html index f1a7e40..050f837 100644 --- a/templates_options/templates_options.html +++ b/templates_options/templates_options.html @@ -25,14 +25,6 @@ border-bottom: 2px solid #4a7c59; } - h3 { - font-size: 16px; - font-weight: 600; - color: #333; - margin-top: 24px; - margin-bottom: 12px; - } - /* Tabs */ .tab-bar { display: flex; @@ -62,6 +54,15 @@ font-weight: 600; } + .tab-btn-settings { + margin-left: auto; + font-size: 16px; + padding: 10px 14px; + color: #aaa; + } + + .tab-btn-settings.active { color: #4a7c59; } + .tab-content { display: none; } .tab-content.active { display: block; } @@ -119,6 +120,14 @@ margin-bottom: 10px; } + select { + padding: 8px 10px; + border: 1px solid #d0d0d0; + border-radius: 6px; + font-size: 13px; + background: white; + } + /* Editor */ .editor-toolbar { display: flex; @@ -159,7 +168,6 @@ border: 1px solid #d0d0d0; border-radius: 4px; font-size: 12px; - background: white; cursor: pointer; color: #555; } @@ -213,7 +221,7 @@ color: #4a7c59; } - #editor-area { + .editor-area { width: 100%; min-height: 220px; max-height: 500px; @@ -228,21 +236,25 @@ color: #333; } - #editor-area:focus { + .editor-area:focus { border-color: #4a7c59; box-shadow: 0 0 0 2px rgba(74, 124, 89, 0.15); } - #editor-area:empty::before { + .editor-area:empty::before { content: "Sehr geehrte/r ..."; color: #aaa; } - #editor-area ul, #editor-area ol { + .editor-area ul, .editor-area ol { padding-left: 24px; margin-left: 8px; } + .editor-wrapper { + margin-bottom: 10px; + } + /* Sync indicator dot */ .sync-dot { display: inline-block; @@ -251,23 +263,12 @@ border-radius: 50%; margin-right: 6px; vertical-align: middle; + flex-shrink: 0; } .sync-dot.in-sync { background: #4caf50; } .sync-dot.out-of-sync { background: #f44336; } .sync-dot.unknown { background: #bbb; } - .sync-indicator { - display: flex; - align-items: center; - font-size: 12px; - color: #777; - margin-bottom: 12px; - } - - .editor-wrapper { - margin-bottom: 10px; - } - /* Buttons */ .btn { display: inline-block; @@ -299,6 +300,11 @@ } .btn-danger:hover { background: #fdf0ef; border-color: #c0392b; } + .btn-sm { + padding: 5px 10px; + font-size: 12px; + } + .btn + .btn { margin-left: 8px; } /* Template list */ @@ -318,7 +324,6 @@ } .template-item:last-child { border-bottom: none; } - .template-item:hover { background: #fafff8; } .template-item input[type="checkbox"] { @@ -352,40 +357,13 @@ font-weight: bold; font-size: 14px; } - .template-actions .push-btn { - color: #4a7c59; - } - .template-actions .push-btn:hover { - background: #e8f0eb; - border-color: #4a7c59; - } - .template-actions .pull-btn { - color: #2874a6; - } - .template-actions .pull-btn:hover { - background: #e8f0fb; - border-color: #2874a6; - } - - .template-actions .edit-btn:hover { - background: #e8f0eb; - border-color: #4a7c59; - color: #4a7c59; - } - - .template-actions .delete-btn { - color: #c0392b; - } - .template-actions .delete-btn:hover { - background: #fdf0ef; - border-color: #c0392b; - } - - .toolbar { - display: flex; - gap: 8px; - margin-bottom: 12px; - } + .template-actions .push-btn { color: #4a7c59; } + .template-actions .push-btn:hover { background: #e8f0eb; border-color: #4a7c59; } + .template-actions .pull-btn { color: #2874a6; } + .template-actions .pull-btn:hover { background: #e8f0fb; border-color: #2874a6; } + .template-actions .edit-btn:hover { background: #e8f0eb; border-color: #4a7c59; color: #4a7c59; } + .template-actions .delete-btn { color: #c0392b; } + .template-actions .delete-btn:hover { background: #fdf0ef; border-color: #c0392b; } #no-templates { padding: 24px; @@ -401,6 +379,84 @@ display: none; } + /* Sync bar */ + .sync-bar { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 8px; + margin-bottom: 16px; + font-size: 13px; + } + + .sync-bar-label { + flex: 1; + display: flex; + align-items: center; + gap: 6px; + color: #777; + font-size: 12.5px; + } + + /* Inline editor (hidden by default) */ + #tpl-editor-panel { + display: none; + margin-bottom: 16px; + } + + #tpl-editor-panel.open { + display: block; + } + + /* Collapsible import */ + .collapsible-header { + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + font-size: 13px; + color: #777; + padding: 8px 0; + user-select: none; + } + + .collapsible-header:hover { color: #4a7c59; } + + .collapsible-header .arrow { + transition: transform 0.15s; + font-size: 11px; + } + + .collapsible-header.open .arrow { + transform: rotate(90deg); + } + + .collapsible-body { + display: none; + padding: 12px 0; + } + + .collapsible-body.open { + display: block; + } + + /* List header */ + .list-header { + display: flex; + align-items: center; + margin-bottom: 12px; + } + + .list-header h3 { + font-size: 16px; + font-weight: 600; + color: #333; + flex: 1; + } + /* Sync settings */ .sync-status { display: flex; @@ -412,29 +468,14 @@ margin-bottom: 16px; } - .sync-status.connected { - background: #e8f5e9; - color: #2e7d32; - } - - .sync-status.disconnected { - background: #fff3e0; - color: #e65100; - } - - .sync-status.error { - background: #ffebee; - color: #c62828; - } + .sync-status.connected { background: #e8f5e9; color: #2e7d32; } + .sync-status.disconnected { background: #fff3e0; color: #e65100; } + .sync-status.error { background: #ffebee; color: #c62828; } .form-group { margin-bottom: 12px; } .form-group label { margin-bottom: 4px; } - .form-row { - display: flex; - gap: 12px; - } - + .form-row { display: flex; gap: 12px; } .form-row .form-group { flex: 1; } .sync-actions { @@ -457,6 +498,18 @@ border: 1px solid #e0e0e0; display: none; } + + /* Footer info text */ + .footer-info { + font-size: 12px; + color: #999; + font-style: italic; + margin-top: 6px; + padding: 8px 12px; + background: #f9f9f9; + border: 1px dashed #e0e0e0; + border-radius: 6px; + } @@ -467,162 +520,241 @@
- +
- +
- -
-
HTML-Dateien importieren
-
Wähle eine oder mehrere .html Dateien aus (z.B. vom Netzlaufwerk).
Dateiname = Vorlagen-Name. Bestehende Vorlagen mit gleichem Namen werden überschrieben.
- -
- - -
- - -
-
Neue Vorlage erstellen
-
- - - - - -
-
- - - - -
- -
- -
-
-
- - -
- - -
- - - -
- - -
-
-
- - - -
-
- - -
-
+ +
+
Sync-Status unbekannt
-
- - - + + +
+ + +
+
+
Neue Vorlage erstellen
+
+ + + + + +
+
+ + + + +
+ +
+ +
+
+
+ + +
+ + +
+ + + +
+ + + +
+
+ +
+ +
+ + +
+ + + +
-

Gespeicherte Vorlagen

-
- - +
+

Vorlagen

+
+

Keine Vorlagen vorhanden.

+ +
+ + +
+ + +
+
+ Aus Dateien importieren +
+
+
Wähle eine oder mehrere .html Dateien aus (z.B. vom Netzlaufwerk). Dateiname = Vorlagen-Name.
+ +
+ + +
+
- +
- -
-
+ +
+
Sync-Status unbekannt
-
- - - -
+ +
E-Mail Signaturen verwalten
-
Hier kannst du die Signaturen deiner Thunderbird-Identitäten bearbeiten. Änderungen werden direkt in Thunderbird übernommen.
+
Bearbeite hier den persönlichen Teil deiner Signatur (Name, Abteilung, Kontaktdaten). Der gemeinsame Fußbereich (Infoblock, Links, Banner) wird automatisch angefügt.
-
-
- - - +
+ +
+ + +
+
- +
+ +
+ +
+ +
+
+ + +
+ + + +
+
-
+ +
-
+ + +
- - + +
+ +
+ + +
- +
Benutzer & Abteilung
-
Dein Name wird bei Änderungen im Commit gespeichert, damit nachvollziehbar ist wer was geändert hat.
+
Dein Name wird bei Änderungen gespeichert, damit nachvollziehbar ist wer was geändert hat.
@@ -637,11 +769,11 @@
-
- - +
Du erhältst Vorlagen aus deiner Abteilung + dem gemeinsamen Ordner (_gemeinsam).
@@ -650,7 +782,7 @@
Git-Repository Verbindung
-
Verbinde das Plugin mit einem Gitea/Forgejo Repository, um Vorlagen zentral zu verwalten und zwischen Mitarbeitern zu synchronisieren.
+
Verbinde das Plugin mit einem Gitea/Forgejo Repository, um Vorlagen zentral zu verwalten.
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' ? `` : ''; - const pullBtn = template.folder - ? `` + const pullBtn = hasServerUpdate(template) + ? `` : ''; item.innerHTML = ` - + ${template.name}${folderBadge}
@@ -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 = ''; + 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 = ''; 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); });