Auto-Update über Gitea einrichten + Web-Editor + Sync-Verbesserungen
- Thunderbird Auto-Update: update_url im Manifest, updates.json, release.sh - .xpi neu gebaut (mit update_url, ohne defaults.local.json/Token) - README + CLAUDE.md: Auto-Update-Doku, Repo muss public bleiben - web-editor/ (Node/Docker WYSIWYG-Editor) hinzugefügt - gitea-sync.js + templates_options: bestehende Anpassungen Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -933,7 +933,13 @@
|
||||
</div>
|
||||
<div class="collapsible-body" id="footer-body">
|
||||
<div class="card">
|
||||
<div class="card-desc">Der Fußbereich wird automatisch an alle Signaturen deiner Abteilung angefügt (Banner, Links, rechtliche Angaben). Änderungen gelten für alle Mitarbeiter der Abteilung.</div>
|
||||
<div class="card-desc">Der Fußbereich wird automatisch an die Signaturen angefügt (Banner, Links, rechtliche Angaben). Wähle, ob du den <strong>gemeinsamen</strong> Fußbereich (gilt für alle Abteilungen) oder den deiner <strong>Abteilung</strong> bearbeitest. Hinweis: Eine vorhandene Abteilungs-Version überschreibt beim Anwenden den gemeinsamen Fußbereich.</div>
|
||||
|
||||
<label for="footer-scope">Geltungsbereich</label>
|
||||
<select id="footer-scope" style="margin-bottom:10px;max-width:320px;">
|
||||
<option value="shared">Gemeinsam (alle Abteilungen)</option>
|
||||
<option value="department">Abteilung</option>
|
||||
</select>
|
||||
|
||||
<label>Fußbereich</label>
|
||||
<div class="editor-wrapper">
|
||||
|
||||
@@ -1119,6 +1119,42 @@ function combineSignature(header, footer) {
|
||||
return header + '\n' + FOOTER_SEPARATOR + '\n' + footer;
|
||||
}
|
||||
|
||||
// Wendet den Footer lokal auf alle eingerichteten Identitäten an,
|
||||
// indem der vorhandene Header (lokal) mit dem neuen Footer neu kombiniert wird.
|
||||
// Funktioniert auch ohne hochgeladenen Server-Header.
|
||||
async function applyFooterLocally(footer) {
|
||||
const sourceMap = await getSigSourceMap();
|
||||
let applied = 0;
|
||||
for (const identity of allIdentities) {
|
||||
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);
|
||||
if (!srcIdentity) continue;
|
||||
header = extractHeader(srcIdentity.signature || '');
|
||||
} else {
|
||||
header = extractHeader(identity.signature || '');
|
||||
}
|
||||
|
||||
// Unangetastete, leere Konten nicht mit einem reinen Footer versehen
|
||||
const alreadyManaged = (identity.signature || '').includes(FOOTER_SEPARATOR);
|
||||
if (!header.trim() && !alreadyManaged) continue;
|
||||
|
||||
const fullSig = combineSignature(header, footer);
|
||||
if (fullSig === identity.signature) { applied++; continue; }
|
||||
|
||||
await browser.identities.update(identity.id, {
|
||||
signature: fullSig,
|
||||
signatureIsPlainText: false,
|
||||
});
|
||||
identity.signature = fullSig;
|
||||
applied++;
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
|
||||
// ── "Vorlage laden" Button ──
|
||||
|
||||
document.getElementById('sig-load-template').addEventListener('click', async () => {
|
||||
@@ -1328,45 +1364,94 @@ document.getElementById('sig-sync-refresh').addEventListener('click', async () =
|
||||
// ── Footer Editor ──
|
||||
|
||||
const footerEditorArea = document.getElementById('footer-editor-area');
|
||||
const footerScopeSelect = document.getElementById('footer-scope');
|
||||
|
||||
setupToolbarCommands('footer-toolbar', footerEditorArea);
|
||||
setupImageInsert('footer-insert-image', 'footer-image-file', footerEditorArea);
|
||||
|
||||
// Keep the "Abteilung" option label in sync with the selected department
|
||||
function updateFooterScopeLabel() {
|
||||
const dept = document.getElementById('sync-department')?.value || '';
|
||||
const deptOption = footerScopeSelect?.querySelector('option[value="department"]');
|
||||
if (deptOption) {
|
||||
deptOption.textContent = dept ? `Abteilung: ${dept}` : 'Abteilung';
|
||||
deptOption.disabled = !dept;
|
||||
}
|
||||
}
|
||||
|
||||
function currentFooterScope() {
|
||||
return footerScopeSelect?.value === 'department' ? 'department' : 'shared';
|
||||
}
|
||||
|
||||
// Default-Auswahl: Abteilungs-Footer bevorzugen, wenn einer existiert.
|
||||
// Sonst gemeinsam. Wird nur für die Vorbelegung genutzt — manuelles
|
||||
// Umschalten bleibt erhalten.
|
||||
async function pickDefaultFooterScope() {
|
||||
const dept = document.getElementById('sync-department')?.value || '';
|
||||
if (!dept) return 'shared';
|
||||
try {
|
||||
const res = await browser.runtime.sendMessage({ action: 'loadFooter', scope: 'department' });
|
||||
if (res && res.success && res.html) return 'department';
|
||||
} catch (_) {}
|
||||
return 'shared';
|
||||
}
|
||||
|
||||
// Race-Schutz: nur das Ergebnis des zuletzt gestarteten Loads anwenden
|
||||
let footerLoadSeq = 0;
|
||||
|
||||
// Load footer for the currently selected scope into the editor
|
||||
async function loadFooterForScope(showStatus) {
|
||||
const scope = currentFooterScope();
|
||||
// Gleiche Offline-Prüfung wie bei den anderen Netzwerk-Aktionen
|
||||
if (showStatus && !checkOnline()) return;
|
||||
|
||||
const seq = ++footerLoadSeq;
|
||||
const loadBtn = document.getElementById('footer-load-button');
|
||||
if (showStatus && loadBtn) loadBtn.disabled = true;
|
||||
if (showStatus) showFooterStatus('Lade...', '#777');
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({ action: 'loadFooter', scope });
|
||||
// Veralteter Lauf: ein neuerer Load wurde gestartet → Ergebnis verwerfen
|
||||
if (seq !== footerLoadSeq) return;
|
||||
if (result && result.success) {
|
||||
footerEditorArea.innerHTML = result.html || '';
|
||||
if (showStatus) {
|
||||
const where = scope === 'shared' ? 'gemeinsamer' : 'Abteilungs-';
|
||||
showFooterStatus(result.html ? `${where}Fußbereich geladen.` : `Noch kein ${where}Fußbereich vorhanden — du kannst einen anlegen.`, result.html ? 'green' : '#e65100');
|
||||
}
|
||||
} else if (showStatus) {
|
||||
showFooterStatus(result?.error || 'Fehler', 'red');
|
||||
}
|
||||
} catch (err) {
|
||||
if (seq === footerLoadSeq && showStatus) showFooterStatus('Fehler: ' + err.message, 'red');
|
||||
} finally {
|
||||
if (seq === footerLoadSeq && showStatus && loadBtn) loadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
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 === '<br>')) {
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
|
||||
if (result && result.success && result.html) {
|
||||
footerEditorArea.innerHTML = result.html;
|
||||
}
|
||||
} catch (_) {}
|
||||
updateFooterScopeLabel();
|
||||
// Standardmäßig den Abteilungs-Footer vorbelegen, falls vorhanden
|
||||
if (footerScopeSelect) footerScopeSelect.value = await pickDefaultFooterScope();
|
||||
await loadFooterForScope(false);
|
||||
}
|
||||
});
|
||||
|
||||
// Reload editor content when switching scope
|
||||
footerScopeSelect?.addEventListener('change', () => loadFooterForScope(true));
|
||||
|
||||
function showFooterStatus(message, color) {
|
||||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||
}
|
||||
|
||||
// Load footer from server
|
||||
document.getElementById('footer-load-button').addEventListener('click', async () => {
|
||||
showFooterStatus('Lade...', '#777');
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
|
||||
if (result && result.success) {
|
||||
footerEditorArea.innerHTML = result.html || '';
|
||||
showFooterStatus(result.html ? 'Fußbereich geladen.' : 'Kein Fußbereich für diese Abteilung gefunden.', result.html ? 'green' : '#e65100');
|
||||
} else {
|
||||
showFooterStatus(result?.error || 'Fehler', 'red');
|
||||
}
|
||||
} catch (err) {
|
||||
showFooterStatus('Fehler: ' + err.message, 'red');
|
||||
}
|
||||
});
|
||||
document.getElementById('footer-load-button').addEventListener('click', () => loadFooterForScope(true));
|
||||
|
||||
// Save & push footer
|
||||
document.getElementById('footer-save-button').addEventListener('click', async () => {
|
||||
@@ -1376,16 +1461,57 @@ document.getElementById('footer-save-button').addEventListener('click', async ()
|
||||
return;
|
||||
}
|
||||
|
||||
if (!checkOnline()) return;
|
||||
const scope = currentFooterScope();
|
||||
const saveBtn = document.getElementById('footer-save-button');
|
||||
saveBtn.disabled = true;
|
||||
showFooterStatus('Speichere...', '#777');
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({ action: 'pushFooter', html });
|
||||
if (result && result.success) {
|
||||
showFooterStatus('Fußbereich gespeichert & hochgeladen!', 'green');
|
||||
} else {
|
||||
const result = await browser.runtime.sendMessage({ action: 'pushFooter', html, scope });
|
||||
if (!result || !result.success) {
|
||||
showFooterStatus(result?.error || 'Fehler', 'red');
|
||||
return;
|
||||
}
|
||||
|
||||
// Neuen Footer direkt (lokal) auf die Thunderbird-Identitäten anwenden —
|
||||
// funktioniert auch ohne hochgeladenen Server-Header.
|
||||
const footer = await getFooter();
|
||||
const applied = await applyFooterLocally(footer);
|
||||
|
||||
// Geänderte Header zusätzlich hochladen (best effort), damit andere
|
||||
// Geräte beim nächsten Sync den kombinierten Stand erhalten.
|
||||
try { await browser.runtime.sendMessage({ action: 'pushSignatures' }); } catch (_) {}
|
||||
|
||||
for (const id of allIdentities) {
|
||||
sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
|
||||
}
|
||||
saveSyncHashes();
|
||||
updateSigSyncIndicator();
|
||||
// Aktuell geöffneten Signatur-Editor neu rendern
|
||||
sigIdentitySelect.dispatchEvent(new Event('change'));
|
||||
|
||||
// Wenn "gemeinsam" gespeichert wurde, aber ein Abteilungs-Footer
|
||||
// existiert, verdeckt dieser den gemeinsamen beim Anwenden.
|
||||
let shadowed = false;
|
||||
if (scope === 'shared') {
|
||||
try {
|
||||
const dept = await browser.runtime.sendMessage({ action: 'loadFooter', scope: 'department' });
|
||||
shadowed = !!(dept && dept.success && dept.html);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
const where = scope === 'shared' ? 'Gemeinsamer' : 'Abteilungs-';
|
||||
if (shadowed) {
|
||||
showFooterStatus('Gemeinsamer Fußbereich gespeichert — aber deine Abteilung hat einen eigenen Footer, der ihn überschreibt. Zum Anwenden den Abteilungs-Footer bearbeiten/löschen.', '#e65100');
|
||||
} else if (applied > 0) {
|
||||
showFooterStatus(`${where} Fußbereich gespeichert & in ${applied} Signatur(en) übernommen!`, 'green');
|
||||
} else {
|
||||
showFooterStatus(`${where} Fußbereich gespeichert. Hinweis: keine Signatur aktualisiert — erst eine persönliche Signatur speichern/hochladen.`, '#e65100');
|
||||
}
|
||||
} catch (err) {
|
||||
showFooterStatus('Fehler: ' + err.message, 'red');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1616,6 +1742,7 @@ async function loadDepartments() {
|
||||
if (dept === savedDept) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
updateFooterScopeLabel();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
@@ -1652,6 +1779,8 @@ document.getElementById('sync-department').addEventListener('change', async () =
|
||||
config.department = document.getElementById('sync-department').value;
|
||||
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
|
||||
|
||||
updateFooterScopeLabel();
|
||||
|
||||
if (config.department) {
|
||||
try {
|
||||
// Pull templates for new department
|
||||
|
||||
Reference in New Issue
Block a user