From 78a63104245965ed1af23142c30f94e1f8ace7d2 Mon Sep 17 00:00:00 2001 From: Kendrick Bollens Date: Mon, 20 Apr 2026 22:46:54 +0200 Subject: [PATCH] =?UTF-8?q?UX-=C3=9Cberarbeitung,=20Signatur-Bausteine,=20?= =?UTF-8?q?QoL-Verbesserungen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neues Layout: Inline-Editor, aufklappbarer Import, ⚙-Tab - Signatur Header/Footer Baustein-System (Footer pro Abteilung) - Signatur-Quelle Dropdown (Eigene / = andere@) - "Vorlage laden" mit Platzhalter-Ersetzung (Name, Email, Abteilung, Tel, Fax) - "Signatur speichern" pusht automatisch zum Server - Footer-Editor mit auto-load beim Aufklappen - Abteilungswechsel synct Footer + Templates neu - "Aktualisieren" Button = Pull + Push in einem Schritt - Vorlagen: Checkbox "Für alle Abteilungen" - Löschen vom Server für alle möglich - Toolbar für Signaturen gleichwertig mit Vorlagen-Toolbar - Base64 whitespace-Fix für Gitea API - Offline-resilient (Cache-Fallback, graceful error handling) --- lib/gitea-sync.js | 274 +++++-- lib/mdi/mdi-editor.css | 1 + templates-reply-hotel.xpi | Bin 420568 -> 424407 bytes templates_options/templates_options.html | 488 ++++++++----- templates_options/templates_options.js | 890 +++++++++++++++-------- 5 files changed, 1117 insertions(+), 536 deletions(-) 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 8bf1a4f28eca2de83bdbbbea73bb294007dd8d4d..a9ed110b835824c1f41031a7bd71a9d1f4dfda8e 100644 GIT binary patch delta 21748 zcmYhCLy#^^uwdJ^ZQHhO+qV6+ZQr(S+qP|+w~hP$n3xwc8F3q7y%rh}kcTx85c2=cu(WnF zH>P*>b}(hIaoxq=cDNRM=rwF462p_)2?3KTL8B@S6DgFm2NtHTJ+?z}#p7+tC6H+x z0X+gmI)aYE>-&Q0|PG6pFq4{fWN&i2SXrv&3v1?Ddu)(ty6?7PuqblH?ZQlX)cvs- z&2+@;^SghN>DqxkzRpVRDT4fP31~jjo}&3_>hVbcrZ{j`>FY>?xSz{gqT$7k!6Y&% z8VP!?#?XJglnp#@n7^!50&U#p9m~P7SNU*N7%2Ls()(h2)zDCLHv^$JT1KH$k zh#}N3Rw!klpO2S|0X%=oplfguY}>Dy5S!gdoZbxmH<5X}DJwExi5%f8L}i$e(X8{0 z*~9};tXivcoB{|TMmMt8j#I)j7<{& z^q7|HkS)Ef^>df&W1c#B2WPWOMvn--{ghcYLMqeN*^^Bqyr9VVq{kEEGQVRxeM{el zn$xz4-w>zFFUeOsrZ`!7cEEvd=SvBf-;Hr(+C-;DK+$+l6h1kPBaWg|xnOJSWnco$ zJu`Z~g=`&JVoY)0u_1B=Y+PON8kf=lYu%9NwN+JWFNTbOT(;p@kx0oP8#WOYe3HMW z%_IgodFjuGFc3D64<|P}9;YuZ?LtbszUt(jJGD99^E2#zryC>7ZO4Gp0-q;Dw;tb+ zAZx_+JEl70V8?(%kdMBEdH0jXUKd~4f1S}!fkgM!{LPm}@vkSx2ZmK5g#W$(Y*3-C zB|84GBL6gP*T&z}Yk=dd#T$P|KmX1RJ`R2is;X9=svqMcZTIW4cU0U};yb5e-i!U= zjBNUZ3*7a`G6SzE3=G0M-ckn+E9@DIHML~V^|@0(`HXlDkC|3qJ9vk}7tHQ90Loa` z*6W}y=~nU8#@P8(j_>CesFU9Vq+4y)B4+md(RWyGw{-(x>rFf_az)O$0FEFkzn*%de~~%hIS89cN9PmuoVl@IDX=& ztq1M=pf5MGYS8wyLsa0DkRon@oozg?@ z-ASc6BoLt(zt>5{Z*I5(w2^xg^k~f2Kb9(iY_awAIHXF4tcd^VDD$BYsYt=(BBknr znsDRN>UiHs#_=?5r%!IKQe7w2P3!S}+)ln_Xk|lNRbyYhQ3|?NxDl)nDh3=XHCt3r z_LGR^aUOrLTm(E9d~K1}XSw47_uurr+rN#TBUPzW`L{)NYQQOA{lntJKITei-42yKhV$Zgj+Hvo>rvm51s7VIC$3Le z?~0a0-V=F8Y*+(#qIdfG^hcQr*a&ACob}=i4E&ooa)e3%E;Lf#pJ0hcI4neqk4S-l zr){pfQ-LO*b*qD5)8WmI5 zvPO7}!hXB}gybgWUCgWQB7VtTLJ^UYVw60zf`l7uTjlBp5?v!Dd1w9NGt~hK({q5_ zr0XB}O&c?FSM~Pkipfiu{%a7cZsr6cu}eoFMFJ~>OmqAY*2C)s#hAvGuYaxj{H*g0 z3)Ms3TP&U}?3A}za>NhLTM2&R9iNFT{gfSWwoN_yq3`QbudCZh1K4-Do z6QUGCYKWu{wZ=>sDiZTv+~|UtwF7`#*fc*^>-({j{ENH# zgMIx~V`dK|iotBQonKctjcSel+jcUn@ZnrYCEF(ti-buwCLK38DPs zEYy+Bs&CV2F0S#~!_bgN_DC-PmYZRW_!ioXM=@}t8p9|f*ZCMnZr21Efg96}G33MS z8p1Dg60I(7jLveU@;dx7)E;$otGfcIrDso>3TGmgb}`7A54Sa+&xwF##&H9qVo(62 zKB_PZdIqsURR=T}Yg}>9%o;!$c_@%1J2`tx)NXQC@u1M|Pd9<9d6GCKF`^JB=W>%h z@L8W@X(WhE0Shsg zJEN^&=Lm&LiI964$GBNK>4)1VZtM)0n>2zE>@f^8s*vD8Je2+ZqL4c@$J-R+lP&5e zdM9vIP3g#{DcBdYxaxaZ^y^I8+Zyy}w`aWz;Gzq_=kAk!Q_}xvsT4koFwOyg>gsko zl$TJSwXvz0gYFx-r@}MYhj;Aihxn)PvXeIC3DR8zcdlctzE9rx@EAR~r*U%~{FQcQ zv-KpUFQ~KjkLV$&eyDF1EVcT_qK}bm%1vw7*X%KFWTshs!RFjdCGWKPa98mG>{_5K ziwny7o=rii2#N{?Z$DQz2Zw;YOM;x3l||d$U%7VKxdS{^k^KAIycCDGI1PNAPxg~j z<41x#USH9~sXsM*-Ij8T2eu0WuoDT%z(1d6R@(k)^HT^Qi$YEpePp;$b~!Lzua)`p zpFO&xz6-{5t^OTZUhXQUTN@F%aX2@RpdLzFXg%U*H(ew4(wU;P0XKl{nIQk9mV@=4 zPFgDACgX@GbK03Q^;5&d#V9n`=}+gr+5GcuO-^eJQ0Kp8?vhM_I?_TkKVzOby0-7L zYekz}*@{u=+Ri95*U5YX!OF}SXe)bK?nm{Boq-*leLYzfbwjOD@WEa+${9rVQ56Neja zuq5gz=*t~Z=M0mcRx5LzxEq3HWu(0$5!NzauwWiKHaCtfbp%zp1qlB}%)%2p;{(aoLx@mql9DTg+znNMBo@8Z3fQ_m`?Y z$7Bpg98O}ftnhJvS^Fu}vhY|d5!L#1)$-A7Oe9;XJWL@)080#*Lxh6*#!J0zMf^N_ zI>iTJMwvad>S}@-OD+qn@#e-}Zu>D;y6}$shJbeC^#ia*uGtMgDVGQb)yE7oKk$~D zppBCKQfvp*O{~>f(XpzfV4XL{3G{K}0Mi|FQ)?+qCJH0Kb|SCO-IEIiRlcyloj57Y z8vs+TCZP~KZ5iW#nrK=u?7>0IV*te3SScAp`^~P4cF=>d88O)Mp%uCM&c670)>!r= z;1LMFdk3hm-kRz^I|CWxy5BxpyhDj-yHSEedF9JBCni_AYd)hR%wgE}h^Cky*8^tJ z4FATSS{ScLNbc3a7R^c8Ng!0K#0cowp{bOcxT41U0~9{%l*Yn#>pcrgD#| zP4r!JCxc6}CKgO-Zils_4B=Q86wn!^ty2<=%1Hj4_JCc#Mlu1kaUk8r-5W z;{i$eOMtt&U`b22yKxKoZcJ2O-86{0X&++2;zpavq9p&+ti0@}fC;+PUKMGzxWXG4v@*ZmUp7)W5`~%P$_z2Bo zU%o6WZRq}q1*u_A)tVz-`t*QG{_*>($9w42TB?E(zssg7h{L>a^J3A3HsFb)0G+A_ z8*zP6#edc>b`7TNonO>{e|Wa4Ea5xtEK|OS_x`HJk^^PRkL}^4lp728J>1GUJ#80AkBIR_4dS?nm4L82CY^o|o1K~uZ~Fh-91T?^ zR?wM5i5=c6B(BdpAihg8f52Q(Op!)zK^Y*{eopUO;Z>#o_QF{h&fr^;oW-U!6z^Ain}W`m=Wb1Y z(;cyOi8BtQFwqP^odYVi(6wOQ)lQnA<7SI12PPn{#=w3EhRce7c<5oWvdc6=aR``9 zU@3~Axy|=6kcFB0)cGMN=Faw_t#x>Sxdq{C47-axah4jVTqc<%L*yCP_W>z6_BXYl z5v(C2-9H^u=0h3{g4y=>hIp3cas9wJWZ=*;yqT?eQs*(O}wmm~$%4-4mtKH695yxmbpJIM*YG5_sN=531isY}uJX;g% z7=Q7N=Ec4^T6*J}(AXsXgLbhH)=c*-LMzZbv_&T@8t6lgSJ*xh5dn`OgMsS))*$a6 zS6(T8D?!UqV4|0IxF+swA-+^}exP;cu!+4TcCDq)xE~u`hZHj?GrhLmc>euQxpX(_ zKj64t(*j=^YB#*CwWCkKf|(hhqv$IMX;nB+fdWrVdGJA^H&GgwHMyD&7aN97^ z`>6jl9Dm{Qq3~P(4gf6Yd#m!9AsUO_<&lR?-L*@?*s0s8AVe=r$5u(@*G^II^aYJ) zaVCNEsGb|KLyDvJNzCmhrVi~*yw2y4Vp7_ zlJ(+{MvTP^PKKts>m~xfGwdVr70j@FJY4>~`k^)zF*b%YAJ}VV;vz+*zHSpt? zDEZ|j zX-Br?@f2C_vb0G}$}qIi9u4JxH1K@E;ww~q=BPdyMQErFBBD#kzHv7ck^J8M$KMI! zK<}9U>nnkPG$KN*h$5_rfd2#JKS2Kj>_5QUt%xAnpx`wkQuRJ3NHJW2fq=9#XsICx z0n-Y@Oi1EyKQOekS{;9Z{Y!8|sZd~l1LKb6P^wY8oA6t~U-q#%3&DeNsiX1oP3QH# z8kx49MWl@KK_wJ9>(B{Q(tDs0HK5Jr57a6*tJ${5t5Gj&#<|sXb9$ZhFjdOdYB*lu z5)KSc?XTyo26k_r$JhSe8@>+0=VBKu=0%TXxzO_Vspt0Yt;~Hk(6`p~euq9me^<`&8@l(Ea`P`r|zP#z> zeHQy^Pxa z4Q(j$HX~KMOFAA}>J`A)SgPIS>vNyJe|gN<|Fsm6-}eUd_)e0^=Mmj6B{cj&I&F?& zV6>9UCvh-;8SE^BmoNr>oKTY>;I}H!1X!WJdk&Nrh(8LP)0Se;@{xD3$;Z z@KP|r;A=t&z;75ajny5A*}O?&$ASX|@aT02{5Jo&ex@Sel1{cKE*tQDIk$F^En7^> zedi3|5)6N9oHoH_$V~)~I}h-Zlb|${*2lbydE4hppABEUWA6-0_?)MM>Ethl1qgNs zP^@8ROUC;B4Q~~->(YamQK&4 zW8T@`-)G!iV7iOT&@=-$daHLKUg&LR^iB7gy0GD>-`SW!%X_Ep)0$l0vL8gJnl?~6 zX!d(ges_*1OdeXT#ExbPT;uVr1HfQ%fX^2}($gE(m}ez#G$bobS7d>Nlc^qd?S>`3 z!xd8yFWHC{5uas270O>z$IwUg1+^j25WFxNrsiC`z(_nu**G9G;vUpNHHXJULu(cA zq|wEDJ`KjnVnx=*e6Ad}jql@m5x)v<8}j{MH>cv*6dvYSo@C}J7_Y2UWM++2S6GN@L7jjh2c*d3lC6!6 zjywE}`TcOj=02pwL3_kZ0qr#)cpV}Ry5y%z&)l{cjk{HNoO!Nt`hxO8)a|HmQqnGz z-Pg;DVry6%R>HtP?B<^WFy9rZ3>oAKkXpVWA*X4DJe4RV6#ovmnYLA_ZV?c+W(KO2 zdx+?aV2yH6Si#(;f>~PB18hdk0Q{xiPEHF^ryXHk0UdH5=ife)00QkCedp_#+@)4> z`DEDxp`Q3R;rt~Siz(d={J1>36FKJ=k-wTx{k*}Noc~C_+PYMLx*+FJ?uGYgIAeKg zf+~S4P+z<-xUHAuwr=Vu8ou?2T~l4mW_B*tq*sXTmV93-aP@=tgBXpek3Ui!_<8q$ zi315G=Wn4(+B8Yn0neZo{=yENVrN4P&=V=3SD*JLB_Q8rT|G5u2FQ(KlWY?YLublp ze_c=>qy4dqt0M(*`6hZXOo#s9~%_^he`puOrYsC33061M0x3xp@XC>^mDs6L; zcs=six7-sO%wsMu%+%K1v)!T&0{HlgXS-jf1XwO?(pi*gj~QA)klqf;V1`$m(PViS zO>oqaW6%VRK|+fpeV1z7zLl5~AEY(zi0Oo;q2rZl|1C#WU$)1y)ED*{H|uRr#TAod zT|^&MgE7$mXTZvZMq6-99;`M`GAD@~+?L#uedVR|X-iFT9E&mjfG?K%(}Rkd>NqAM z%G|{@Aa!>FShtYt2k-VgyqDr`BN)0-&{(+Agy97S%SdAs1VcN8wu8)tYv&Eg$#MQ?nlqO;(a|RXX z!VfIrAT;K`RgzSfjmrIqVY0-vPlnX;+W6HyR&EQR{slxiZ0H=KfZ z%9m}<9>foQGh8DQw{8G@rv>i~k|6&CoSoQl0jt9*>=E4B4*It7bu&B^K^cvRer~%q zjV(4+v_W0{>m42#IBA-@o`{zneg&X^(jfFt>VTr~52vL_IqCLbQhY<}dE|^J&*<(l z64#h_e_ef-)5cQb%8sqIL9UV5MEP9@i!*c0YP2>x6b{5QSX+5nUdw1;Q!G<&R@FUP z0Y{MOP$1M0j~*&Z^@>jJl#P!Xg29dzAc>v)<0GJ9lS3AMtU~sLd#xIvZ#=0{9FjPD zn^1<7Wz_VA8{lApk;9)i5wm4#dzUO}?*Ec_r23*+0TvZ;xQ)=4AbK?fQ+;NXHZPdY?@pGZby)rd z+UaWjr2>OAL_Y8rws4S5^*f8l`E5HHteAw5uR8stzBV=8!eLxO=I-I+jykUGM} z|2fJ5RcSiUo|&U;W7Sx=U3E4M8Hl!c{=La0z?rj+uqq?IYGr|dwmsg0^~6Le0X%4% z*26bF&V1o~Q4USa-CD=Utsm1XbEFW9Hp@?X_}z7$!}0GHk&)*-u?4*nc+TYh6|CLXwDUV1<_s7ATNzhiFor`&M*KOnG zl+@uoP7lMOY-Jw7CD9|3!ys-i0SHQ!t&fXs6xvzY~XA8oc&mb8UQ z72&i=sOdNq5fx02)xLNrtwnQf03vJ8B^a_D z>kV*too4@Z6Q zcGUnkI6Rd6*&`|pogsX&H}BDOnN_RCV1|%@6IFtqv4227)Eb1}F`e)F@+Eqiy#}h) z$p|L$%8&c2a`OOW!&xiK3_#lJ=!3{+DO-0!c#D6fDP)Ifk7F%RE{IY!b~`pQZ_duz zySffBv4-P(roTF#PCU)g;YB?~dovOpHTVCcN(}R|%WzC91XI)rQ7+&;&*i;l-Qi}A zQ2tUglLliDLls-#IBv}En-QBe!ZOHaBZV|~!Q=2ztqev9QCX->1_(|`Zp(_wZtL*g z&VQN}j`cDCuL8&88n-q@4|2y`eC@&=%Ny3@UDbn`-^l!Uh$H&2JK(0 zQXoj-?Cl*p_djUDdFZe|HpM={_c@%etmxl<729+DfjttkVDdJrgk-1lmfTUtCgT?x z8EB$$09MJ_azef{0|Zr;3JC+9oTyi$XGjY*2}_T4%>D6k>_e&a7{bhuaBh#hI0!K7 zJ&JgMQbsBv2C|XQ^Ev7)F5XS?BNTTfl1~Z-e82m@8J0XUPd;#B8S;Sbr$t(+oLyXE ze8>KUse^0dTbs?G%dZvdck6f|J|&t*0WuCHsXJ#^>kTvj1F)$CV0cTbtN+wYULxJM z%Vp;rzOx$R7*D0EVUnxO988fyCOtZ8{IU*93O$ za)=3@5lxcO0elAnbB)h<>CB^hf!DO8kd~m&IBTL+M{&GW;OZ-WY>UN9h44$n_GuJf zciSqL`h!MCt#a$zDF{D-jxLqpm^LtLllIi>LH&ab%LCx9a2a@JLeLmPW*s0H5eiIh zy{Id)sAb=G>v!^pA%R*zmnLxwA#u9EG}e<+b&1iC0dSO+5c7KJ&#*7*8q;YDrE?$} zLu8(HvWDR1w(>GOKm@qt4~lqPryL0L;KZoaU&2K8-U!3liFOZ+U4@CN9B`62AhZQH zX9a#}E@*KZ5k8t5@X7+-!$OC5=QpB=ekq+9psy}Cyn8u+f+#W$T%!68(CuM#kL)^{ zOycEs0ceRr^S3koi=q6kd{&o7&+m8b-G-S*2XpM|)ra8lDw&S*1}UnF5l<5L=pMC` z*}%`kt(Y62_|bpi*9?sJvwCh_=hh63W8EU}0D)7Z z?OYz%OY7E=+N=1)O*_;UZcA}V$UMT~!-BL@@RPaMK% z2G@o%m^xH2gCGgC*|_Ja--#1FC61}D*C`VZiUG=kRpJ((rU7|ujpXZEBqVJ}?6y5-1ZGz%z_(~Y|_heU8WCqO0= z07DhPGnh-8v%B>K61osT3d=xbwSs{F;M0B?)Vuk*eG}HW8QlflKYeB643Oadt}OY)_<2 zWRU{E4%v-ddhYKE16G7(Hv-1^RG3|B>JssvYZc=Tgf(s+zIm(#`6b!a3WbZXC5@nHg&7MG2>J-GU;;I{ETPit{9!t z!VB=}WkpNb@IvhxRYizyH;djUfIl7%j~qob(JXuJ7#s)9jp{f`X(KW4dhUcu?Z6bi zwY|9(h;JpC2UrbS< zc@9T+YVU0+ph{N3VOp=(I&0?726FfGeSmDDExZDZR^uKD>AN$yb7|JqMB$n*3mJq9 zWa%G`(+Tl^;Z(eM1hI;#rKEv$-q`P>mW3c%UM{pL2kaMa`{KwP&Vyt>G5Zvz@&d@` za$v_28)prH1%wC0px-8406ZlaA;&1W3-K19pd(ITdCLqsfqjAr;Uf1><+<-y-Usi3 z2PU>M7~q~nD^(hx&B1&OZK1DpL}Z`gnvJzM2#X4rr%<6nxISu)t6euk7xPR@~an8K*-kO3Hq1)6(n~G zI_$CyBq;h{l3}+v#HxMJSm^YT^hDkMFM2pJ7~CHCjbU+g^mE-LdNn5At>tZEM-a&+ z*E^LB{8p6MIe!5O1Nr-a!B{9d>9)PVG0???Hc_#Heo=!GV!D2K#mbR7DR2{^lmV?4 z6D;#bxj(H%CEKQf0H0fiIzAS{1uzl=T2m+-3B7?M6l@e}%`b3FEE40+;;nRrx7{&u zT8A#LB|ITksb8HG!M>lMo=9q|B0gXcO*fnkx|eR{XmVsV4wiG_H~5YFa;5wgHkSAt z7SAdIVRNx75>NahJJdz2))-qXi>)vlpHax-Ntf5P)Vq8uKz}&{49V4*Lajof>w}rH zMOqMMzyNj(BUZQ`H1{ysCkh*Vgan(>aF4K9z>3ZcQ{6g@f~wA6*i*N)T#=$y4T$U_ zb}C+rO=q2l&#~1CEheEde0VR-rwv~nUUatPAB&cpQ@ak2KkdCe*@Vggh4UYRV%l$R zpKs}|ivOg|0Xxe8i;p(NE4P%>t(WN*+YHNf#_2l4c>Qj;?tl;WW{B!}-3-lE24j@B#FfShHyenZeI=G%vao;QD3!!g zh?+R;U>8BMMcI6(5sX0`LM3URplJgC4HU@d1GH6%04owQi4#&E!L}O8&Al*&#*~Di z)OR42cq%Ooj1 zawMow0JG~C#Qgx+z8a`e!Z|9?lJd-xY_?TQkJO0^o{+{Hv}W`lmc}x2%@@8^+N;eV z5ooKgdO9!Zq;Ac@>sCFA<76cT^*vA2Rz~PD>)UA^dl}e|%ZzC!&eBNp^&o|iYP;4s z1=|;)wvu#fbukie|2KRKTr<$&9B{mV*{3@QK%_YNDU+_jPHA3I9c4XCCf|26^i$?9 zfX{f?iOVDBqMad?Q#3Q_;%7FlC^6QJn=J$3VuY|d-nC!}6L{d>YXFGoa4V4jemdi|ltD~LsTBhQ34GI=>I~e9QOQG8Xki|){Z2$bw^d>g*o9&E7^%L z`S@Eu=9EhrWjoEzC;H9NOX65Qpjz;Ec!9;_I__`+V?l`|$xN*E)^4&21-|V*rb(+8 z+H&Dw-WNF%nXi=~Uld>PfGx~XmTh=1k~#KMQj8W06zg4NLZ1OR`w~~Wq0D>*z@Y+i zLLg7t%j*V56L!4~zuKiSKsU)>WWpFQp<*mH(-%6ZMA4FN)9}D@`{)8=Xm+ssgYyu2 zPG6OoS`T?OKTsMrM>@n*K|7fEdJY9Kc2-eyBWQHgUl(;`i~Fk@L;aWCL2A1nVn3q_ zlhWqV;L2du_&to{q?ZWX1x4)&5Xh|r$zkLMRd<);kB zCl?Q{=^(O`&T|j-`j`zb+t?&s%qzdWH3mdF&Mr>jO*m2Ui?=<`<<lQ@&PTt9}L_J`XT=7;Uws}#+Mu-Z%Eehe$fPmVvB2~IFo6y)U&iU2m3c3ufod0FnxVla|t`(X8q&wNqUS0rixF{7%hjf8PwHN!s#Sg`8mCnG;b7$YfFRZy!5 zP_gRj!yc&+0qW`kCIWy!V|4?}v#CokqoMCbZ6xqxnLbb_!IOob*n$5l{hcG14Fe3HA${2^i){oMgvw|kV{)??&LYAyd8G4AB2g5Th@|Gl<>{dJneh(4 z_igMFmxy+*pfLs>@s~%^`a&`n|98}v|7qN?GXMU=I*V^)zaBt2skw*z@O)j$7{K_w z;fo~S)BLTB=H`Jlst1U0_*htP7b z+Y$wFAy+Iw0%!x05a(^P-VIIcW}5{*WW|?}2xlLv@2{W^3Q4#)n)8ly&V`XMj>4s9 zRwyb+QvZf%&5RR|ar;4ny6KbNv2YL^08eGfp){8+hr4FZ75Qv`>K9$Sf8!3Q@LZRpD!SMGYE9;#ITNwV^ z17jw6;Ojxn$mBV%^*ca@==_&Y`~5F{=qxPc>KBlV35`t1A__;OUN$jdd^%Ig!=Bf>p28$U8|VEth+)jwM@jnh~%y< z=(9)9GHPXtH0GSx=l*-=zhlGu1vVh{fg|H3f8Tw7VC@z zrUlh%(EM)Wkcs9Q1f?~E?@5}FKwYsx^@E1^KpxHPsTUNxsNGd)-*0Z?Dp#Zvp z$bH_0?*-G7tbTBA1b0O0J8yWfH`OtB{O3!!x$XLQ9_oU>8)yCR0W^S+qO zQwUIYNZ`1p=Ma<%5qCj#{!vfRkY?X9^=!Mt|?(JJ~^x{4m0g5ME z3`}L}IW)@RNB@c=w%uJ5rHw_R%n%J@Lt&EXpOhV~@fjB67FDP<{lPhz0X9HB@dWHE z!*lS|$1jOu<*Wzt?p9{R|3iDwFU>;Vf+o-q3t^vpzUhd}|O#=C+F1MXRLb;hz{3%?PBJ8!{E zdEmuF1Vv43^Uv3I9o@F~aAV2fVz1Ha09wXCa8ne>5R@I+BC=Z}fI0KT4}i%MQLacI zgA{HuWJx}6`jBM^Jsg;(fS@TBrw$wwq@sYc<1TNbSWqkA!5;?T>-)lG%Yht&c}rn_ z8xD_;Y%+Uk30`M~}qv<&w|Vc>tt~``)tx%Xk z@ru=Ey=7JvAleM4ypm-h#~+0?W42*`J=B(|52V-NMG z^MyxlVcWb}4W-f(g_YIaa_|i9Sc^sMYEv~kMOfh+=G=s&Q(lsg#I1S-|kui68DFQu!(kwt^guo?p z$@ncy#<*EbzH%HfmjsP4tFwx+r9*78u!xn4A_m}@I|fccS)eI1gty0Yeg}5I&Gy+*ct<+tf^> z+u4WoxI1gvQy!vYdF#}*qw4B(MJ>Pla&)$Jj-o;~)%nn32g zDycRke}*A7Y)%X<51};3Lz7VC0@jS?ouWNJ;spd33sqq(>=fT4{UHapIe13JiO`sX=SC>l}8 zlZnE5xD&h~u$1=iSZ&7kYJb1rp{f&Txj*0jPNm=}&7JjvSd7bPXPb}>dd~wsXOhVC zxKPO0E;9Saq8B28^zO}i@i1%Ipf`JcEWr(QB`oh=tDshOM??3w_V#?07VW%m2L7wQ zh37AafH#hygmWth#o*!|+~CodT%rCY{ekD7VB`^fB=2Ciz15qx-Cob1CDHGBI`v$99}h;iTahI=fFh9?0JVhB0JI&Xb1%uOGA~MGZu;%V zkD-OV4E+)z+}37^w_&TFO30;h-E>rG9xMH+Dw=PNB>s*9l!{0p)oEik1a!}65u_+l z;KWCIuBKbIB+(Vfg8W<5s|+Q_|EW3{FSAo&2f<}-mR+QBLu{z3m*huaBB1) z0G&+*wlY$c5{q`CgMF+7z5@cL9RkKqGMC3(*^Mh^AU zG$+_Lq&NaqxsC8kj31H$18@|@!85StM~AF@MxTw%waG9(xI{8N)(p86p5@7oD1EsT z^wwV(EC~*|GxFFOw%CxUoUdH-Z}nIK0Fdc#HNuofO#n*Q)T_bs0L#^Jav_mq6PV&o z>JNvH1`A@1m8HJ|g~?q+kxzL!{f`hLUu7>M-~RGRN+Pe_}*U|GS$vP8@ea}%AX#W~y|;7=Kt zy2PMn0Gvcr?Lky$G~rq_!O1g`#&zX{RRpj<(`a~sFff(>F!zd6Io&LdD-9GyW_0k4 zmF-SXUYKTVc+rQFb5Gti&mf#J`dfWoPuguwPeL-AND#pr-$Y7w2tsD;c{9*v-?y=~tn%;#gG=JN3LwSn&mxf2 zLO@^z5Y4~7A`+OH)&6t4N)mY-4~=CF7f`;zA;cifrl~-foQYJO|K1xz#ZYOx^_`=G zlG^r)ndu)W$S3=_1~g8kM4N(+hTdL7loSK#xz(wHwx@D~*GMBvPO2mUfK3u-5ov7p zf>4ZZ+bx5m(jkmLv)kPvs0u-v@x7SnbS}aeFbppO%}0F|dL1=x0&>5LqL!8Fvj0R# zWC~Nrk)+f()Jh^y>{00S96^NeeUT$|SnqO5@v&+|3w} zPE}o3QW2EIi#tGMT)hMWFc}@k{*vYSLNle9fHmYo`-x3P3YYJF_L=Q3LxU~wI7M53 z!#H4x;K~kD_VFO@)#C2;cv2hYTkCT{!-7yr$0E8H z764UAgadwP)_RI;ir9w4%~R6Oha|O8%PAx`)tj_NTf0^}T%P9z_z2U?+XlF2FmaOd zMMplkfCl(C6;BZ1&f}K|ah9+gne<9y5ZET6{)(^;Ih$bHu7j096Ims?Tu$n8Gto*T z(W(`f)+DVPt)r4aXEEZbfq&-Fb57=bC^@86*NO_H$jMMk+z78>Er?KjQ+-G@DKe0K zl%yUD;+kJCPlS}^d5jvE$Vd8a zA@b;lN!%l7#XzLCNZ3{OLGGTt9D4;_@;ZZ!2K8}og=g4+n=|EMk0PsGk0Ply(<{t! zXE2)vXroup?3 z9KEjC=8xu&YVNgZ9}e;cUI#p(<6(Y1^m4L62G&59BL;l2i$^#u&P$*AMKBW3{RyK4)R@?Zh7+8@dvFU(VGsL@yt(}K5l3*~g@l*Z zYWJ=X&~mD`x%H6x!9;C0?V2G7ztlvYA!IiU`Xp(UtySins-W+c@B4da7eTiRtxs*1 z-A+%wnBUoqK1q_pX6`Ly6C~qfuMNM6tS3@U5Qa@am_@`-eRboRtkEr;YAsd9Awx%#tDT}cwd4uR^i=Sh5~>dn z9XNfa4x;e-?Zy%sFVM$Fi^2mBNS=QEM@swlr~iD)zwC5Mz3l6H8p^@V^s)GpmGq{? zkMJgFZBADQiNquPx|^l$Kx}))4WH;BkIf=s>3<5j%BU!`MmyBdB}hn@bSN;?P$J!} zASpEL7BOL-mOGryd4$L535+gW-v@}R}2t2*ty>DILdT+gTe(bf+kA3!9=bT^X z?2U$s$Q9;S?4JQUh%I;Gx#=G;;xEGrlY)2%s+l>~G{;Fi{S0a%`n+f&LsThlgf2Ec zqA0$kDJqz2qvDqom6pPip#kycBm-P_#BAS(48K;3h|VP*e|yy#D-rlG}bc=LDi4PkulkQ2btN`|fN(kSANlSjlWy&LYq?GMK{t zF`GAYxO}@0x+*x={mb0-iy&%n>*!3uxm_UQ`5dMH)nkC zt!F(Frp@FnC^IM{1w{n3oNd`7p7_D(OdKP-zfur?PL8n`SRL??>e#1CKh8VOo>1nj z>9bp7(d2KMV$}eB|MjoaG8n_3o04fBcE|SpYqIv43H!LacWCrX-;YZ)CYw9^UyX!f zt(r_J^|XY?J!Tu6Qhf3X&k=Rs#?7E|;px3ZEO^gW>QWy`1OjR-Jgb&%d*L_l*ym4D z6<~chCou`P6)$fB2PS#IIvHmuu{XyF1J$C`Lb*#zkNU`OHCMzM!f(0B%^$qsS&#N2 z*UaC-@Lwu|nLEmuVszIv)zo!MY8dFXTqlA-M7h4!I^*UxAu@KYiRK7xmj>oUdzFOGv{| zq5E2=vX$f+kR2t zk~)=t5?9`g@S}IX3dn~WW^CT=yFqnJZ|#L>2EVR6>4HN`@`qtG0PVONy;hkm@{di~ zmznZeM5z8OFLbMz42^VGd??j+8sn82TJbtNCstQ`yUd^d(a)zj)_^B48DWW(@4;)c zT|z~DmIhD|vFzy*&pXL1K8}>*W+AE!4WR2i;fOjp_cegA6N|hZv8l2YpqahA621>< z7fuS&1KU4Ix#5enm@C+1w8O)Q=Vs4Qs5!<8%*gfJeES7%C{2vNCX-K{riUCD9_N

5MN5(NQe5njb#P~LZl7O%&mwt@G+?F2e$NR_$ou7G( zEqO~DY=AU|$PcA2Gh3N1tO7_XdP?_XlnmI_2q{b$U6*S?f;n|0i9ht28LN=HZ<{1b zjHI29YK^QmsqQ0eA1m%ID|$wkczP={sN95c9{Q7Z80ZOuhrr(t7QUENwh_U*V~wHd zq{rX5F|$}gB3Wx7V*SnK&9^uppJri+bC=d#h%tum)3bstJfZU5M@|&!YKH0*y45Pw zy3z(BTtmb@B$Z;D=jlu~0#g*HF;3Ya+MGMBI*m`f1}Z+9wQp;8Ed{`8&NXrr`|Oexv{F1(E%E<4PyjyX(6|g+WQ&G0s$iy(vcZ1>2XN%XV5o! z$I*jQskQIBTHyK9J^5eD+*0(Uo81%;ZB2`2>nQal}c=Q4}s3nqssUtm^?;DkM zm8PugwBS!X4rpc#>`xfy4=I`>43^_F}wubGkHTe_k+)^ zFN1mxr7xjc%3igY!<0zvBX&iKTF_;m+{Hk_f-IXI7C-8nIoPVMB&s8{=VR3C65L^iZtR6M+5XaTQCsB2mCy5B9YCULO7e2ZQv zArW~F+wIDmZuBVLSZskUxa%lwULsLQlWP-COJ4Djn)E>@p&**h*zc{&Wm#UZ*#UM+ zyB*GZ6_zOxfAovuUl;x(500L50u{;Aa901yYKZU{LAHOfxW^4To@I;^YLq0VLuF2M z4~GgOC=(hBr72q&L(9Bd2E1o;)34C1g)SBh>zr}@zPEfH_VxksTrUmn?DTe`p(|dMo-@`BNDdb&`~}klW}B(L zbgX_R7jB-S6RjgRY>3ezg@TPT{QSt9{2=6NRxlcCY{EU+ZLGiYNkHEFOjM71@se#k z#{FtjMwV@}kLoD3?(Nh?p`q(oi)&1ypxl`{?htBYDX;>r-?W`8RNpFs-v;}~PM4D( z{|F@r`r5%8T$1%P26W3ALT%w|=ZZRDj#_ysfjQ(9T#RKoXyJEG?AIOS-rrEhgu{(7- zyt}kXgdMl-`~3>%pj92hh0Ves_=-u1cL>v<{_TbmLZ$A6x8$>hRFAt)%shqRMF`i1 z{z29myPyCdEMg%kwV)|EspU+rHi)!Fl2s>tK-;o%?0lFqu-qcE)55X*6G6@u-^?yl z?|Jo!+ye>t)Yh|3*K;T$a>2)*r5~^01+Is}ek($%^fKj>Uo3;4Tq4HCCClSXs}Y50 zl8vZ>#X8hu)$!qO4{YoS_0229qfRSX%i|Au{K(EPl{M2X9`juaX50aqI}g{693JiM zeRlKPTn>q2EotOg*C~ z(<&v6!5pzK2*%uJ8op|hxMmU-r=KJyri%74SvNC~*~mhx`QI`LIKbs@!n$z4&qlnp zXy1I5rgPMJ&xLY^0KCWzNaN74lp= zi9n@m)G|}XWhLfAw3SoPZZacI{`u#x;k+a@VAC_MQ2FA<+1P8q#x$2bF zrya``OPCymq{>P=V@#AkR;!6)v)xAW4$)^HZdR;A2{G_e=>#$K_Ji7JR26CC*WQ)> zlv9X1W}wp=eiRTAfwT^&&e(d}pzr_rICE?>_P7R0!;=McY6gY58yAa0a)~DeX6iL7 zGzY=ce50d;Vssy^^pp0pci-@WUi-C)70dO#<)9h(3ee7avvRg}(W!XItE%9O;DvRj z-+AucAZ7{hed%}xSJb)V z;dB7yYo~s<@}203;_jn^HA<*RC`2N`bD(NE=)=zK6CZlnSbHHoIsG88i}*9m-Pqcd z`9y6;>ut#h%PcJjBq3hmvmTG6(92i)%CS9l~MaN?mIfwIESJCQnew znhdKqF>OT$^<<`flS*w9T<3$M#|x2%`>EZ~nq&p^-C+3FqweuYH;nxi!Zov<7$GhJ3A#j%hUT%!of zs%!>?gFt$rAnTE&%@ujmp~1G@o}!!jU00Rvl)7$0chy3!0kF=j^%oDW^KA_pPD~V3 zR}{Wo08ZJm>3;@Sx{f!t%&<6h9INh9Rp0cszvCj!U836JmC$x?gdl4CBx7QstfH=k zY|l(ZKUdSFtS&kxO!T@<&AFke`|}*xS9FHVP}5nB7rc6S1WGb|-OK{o{v!P?^<-zc z@e(EZIj49An;Tj0t~82RqA@FSOtFGid(oFNik=edA6GYCQJxByf#S=+RJ=mm&tfRfo5KHQWxjHujs9GQ|Hc>LIABU8v?&w!>gX&^mJZuh9!ZhI#Tzk>1&1}DN zo2yBQA}0%ZQqs!AN-nRr8Xw`8cOYb)CBA)}Z}wbRn9XskJ@9S;I_emhvRF;Rwm>?J z|Fw;h{qJr{vY;LLMAH9sQxX7}|C+@B&;Wv#5RyN?%whzk1007m0FZSF00Ur?dfE!2+Yqtjy-Tg=M zk0oFR4nTg)bOBhX1JH~_R|p7jb9M7_6L5Ix`~t>o2NZ%aLV%2zNMV=^1St7Cf$awY z(qpQN!_px@=End?Od4?Xmu&&z_`mRP{%_lVrnLb8u0K5bi^G0GfMSpDlyUyGMMt^( zf8pQ!-$H+W&I17M{~?6d0>ZA9ft;{hM<6l(KfeA4>Zs!Ye;*H$U&9v%08oAToBkJO CqEA); delta 17876 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 @@ -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); });