Files
hps-thunderbird-templates/templates_options/templates_options.js
Kendrick Bollens bc82e33bf2 Smart-Sync, Scope-Fix, Auto-Refresh, defaults.local.json
- Background-Sync: SHA-Check alle 5s, voller Pull nur bei Änderung
- Sync-Hashes werden nach Pull im Storage geschrieben → grüne Dots
- UI refreshed automatisch bei Background-Sync (storage.onChanged)
- Scope-Badge-Bug gefixt (folderToScope normalisiert den Vergleich)
- defaults.local.json: optionale vorkonfigurierte Verbindungsdaten
- README: Doku für defaults.local.json und XPI-Build mit/ohne Defaults
2026-05-07 10:50:36 +02:00

1691 lines
63 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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';
// ── Toast, Modal, Offline, Token Toggle ──
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('toast-out');
setTimeout(() => toast.remove(), 300);
}, duration);
}
function showConfirmDialog(title, message, confirmText = 'Löschen') {
return new Promise(resolve => {
const modal = document.getElementById('confirm-modal');
document.getElementById('modal-title').textContent = title;
document.getElementById('modal-message').textContent = message;
document.getElementById('modal-confirm').textContent = confirmText;
modal.classList.add('open');
function cleanup(result) {
modal.classList.remove('open');
document.getElementById('modal-confirm').removeEventListener('click', onConfirm);
document.getElementById('modal-cancel').removeEventListener('click', onCancel);
resolve(result);
}
function onConfirm() { cleanup(true); }
function onCancel() { cleanup(false); }
document.getElementById('modal-confirm').addEventListener('click', onConfirm);
document.getElementById('modal-cancel').addEventListener('click', onCancel);
});
}
function checkOnline() {
if (!navigator.onLine) {
showToast('Keine Internetverbindung Synchronisation nicht möglich.', 'error', 5000);
return false;
}
return true;
}
window.addEventListener('online', () => {
document.getElementById('offline-banner').classList.remove('visible');
showToast('Wieder online.', 'success', 2000);
});
window.addEventListener('offline', () => {
document.getElementById('offline-banner').classList.add('visible');
});
// Token toggle + name validation
document.addEventListener('DOMContentLoaded', () => {
const toggle = document.getElementById('token-toggle');
if (toggle) {
toggle.addEventListener('click', () => {
const input = document.getElementById('sync-token');
const icon = toggle.querySelector('.mdi');
if (input.type === 'password') {
input.type = 'text';
icon.className = 'mdi mdi-eye-off';
} else {
input.type = 'password';
icon.className = 'mdi mdi-eye';
}
});
}
// Template name inline validation
const nameInput = document.getElementById('template-name');
if (nameInput) {
nameInput.addEventListener('input', async () => {
const val = nameInput.value.trim();
nameInput.classList.remove('input-error', 'input-valid');
const msgEl = nameInput.parentElement.querySelector('.validation-msg');
if (msgEl) msgEl.remove();
if (!val) {
nameInput.classList.add('input-error');
nameInput.insertAdjacentHTML('afterend', '<span class="validation-msg error">Bitte Titel eingeben</span>');
return;
}
const templates = await getTemplates();
const editId = document.getElementById('template-id').value;
const duplicate = templates.find(t => t.name.toLowerCase() === val.toLowerCase() && t.id !== editId);
if (duplicate) {
nameInput.classList.add('input-error');
nameInput.insertAdjacentHTML('afterend', '<span class="validation-msg error">Titel bereits vergeben</span>');
} else {
nameInput.classList.add('input-valid');
}
});
}
});
// ── Cached author email (for personal folder path) ──
let cachedAuthorEmail = '';
async function getAuthorEmail() {
if (cachedAuthorEmail) return cachedAuthorEmail;
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
cachedAuthorEmail = result[SYNC_CONFIG_KEY]?.authorEmail || '';
return cachedAuthorEmail;
}
// ── 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');
const saveButton = document.getElementById('save-button');
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 = [
'Arial', 'Arial Black', 'Arial Narrow', 'Book Antiqua', 'Bookman Old Style',
'Calibri', 'Cambria', 'Candara', 'Century Gothic', 'Comic Sans MS',
'Consolas', 'Constantia', 'Corbel', 'Courier New', 'DejaVu Sans',
'DejaVu Sans Mono', 'DejaVu Serif', 'Droid Sans', 'Droid Serif',
'Franklin Gothic Medium', 'Garamond', 'Georgia', 'Gill Sans',
'Helvetica', 'Helvetica Neue', 'Impact', 'Liberation Mono',
'Liberation Sans', 'Liberation Serif', 'Lucida Console',
'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Monaco', 'Noto Sans',
'Noto Serif', 'Open Sans', 'Palatino Linotype', 'Roboto', 'Segoe UI',
'Source Sans Pro', 'Tahoma', 'Times New Roman', 'Trebuchet MS',
'Ubuntu', 'Verdana'
];
const availableFonts = FONT_CANDIDATES.filter(f => document.fonts.check(`12px "${f}"`));
const fontInput = document.getElementById('font-input');
const fontDropdown = document.getElementById('font-dropdown');
function renderFontDropdown(filter) {
fontDropdown.innerHTML = '';
const q = (filter || '').toLowerCase();
const matches = q ? availableFonts.filter(f => f.toLowerCase().includes(q)) : availableFonts;
for (const font of matches) {
const div = document.createElement('div');
div.className = 'font-option';
div.textContent = font;
div.style.fontFamily = font;
div.addEventListener('mousedown', (e) => {
e.preventDefault();
fontInput.value = '';
fontDropdown.classList.remove('open');
editorArea.focus();
document.execCommand('fontName', false, font);
});
fontDropdown.appendChild(div);
}
const isOpen = matches.length > 0;
fontDropdown.classList.toggle('open', isOpen);
if (isOpen) {
requestAnimationFrame(() => {
const rect = fontDropdown.getBoundingClientRect();
if (rect.bottom > window.innerHeight) {
fontDropdown.style.top = 'auto';
fontDropdown.style.bottom = '100%';
} else {
fontDropdown.style.top = '100%';
fontDropdown.style.bottom = 'auto';
}
});
}
}
fontInput.addEventListener('focus', () => renderFontDropdown(fontInput.value));
fontInput.addEventListener('input', () => renderFontDropdown(fontInput.value));
fontInput.addEventListener('blur', () => setTimeout(() => fontDropdown.classList.remove('open'), 150));
fontInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const font = fontInput.value.trim();
if (!font) return;
fontInput.value = '';
fontDropdown.classList.remove('open');
editorArea.focus();
document.execCommand('fontName', false, font);
}
});
// ── Editor Helpers ──
function getEditorContent() {
const html = editorArea.innerHTML;
return (!html || html === '<br>') ? '' : html;
}
function setEditorContent(html) {
editorArea.innerHTML = html || '';
}
// ── Toolbar Commands (template editor) ──
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;
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);
});
});
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);
}
const isOpen = matches.length > 0;
sigFontDropdown.classList.toggle('open', isOpen);
if (isOpen) {
requestAnimationFrame(() => {
const rect = sigFontDropdown.getBoundingClientRect();
if (rect.bottom > window.innerHeight) {
sigFontDropdown.style.top = 'auto';
sigFontDropdown.style.bottom = '100%';
} else {
sigFontDropdown.style.top = '100%';
sigFontDropdown.style.bottom = 'auto';
}
});
}
}
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);
}
});
// ── 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 ──
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
btn.classList.add('active');
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
});
});
// ── 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() {
try {
const result = await browser.storage.local.get(TEMPLATE_STORAGE_KEY);
return result[TEMPLATE_STORAGE_KEY] || [];
} catch (error) {
console.error("Error retrieving templates:", error);
return [];
}
}
async function saveTemplates(templates) {
await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
}
// ── Sync Status Tracking ──
let tplSyncedHashes = {};
let sigSyncedHashes = {};
let lastPulledShas = {};
let currentRemoteShas = {};
let allIdentities = [];
function simpleHash(str) {
let h = 0;
for (let i = 0; i < str.length; i++) {
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
}
return h;
}
async function loadSyncHashes() {
try {
const result = await browser.storage.local.get(HASH_STORAGE_KEY);
const data = result[HASH_STORAGE_KEY] || {};
tplSyncedHashes = data.tpl || {};
sigSyncedHashes = data.sig || {};
lastPulledShas = data.remoteShas || {};
currentRemoteShas = { ...lastPulledShas };
} catch (_) {}
}
async function saveSyncHashes() {
await browser.storage.local.set({
[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();
}
function getTplSyncClass(template) {
if (tplSyncedHashes[template.id] === undefined) return 'unknown';
return simpleHash(template.content || '') === tplSyncedHashes[template.id] ? 'in-sync' : 'out-of-sync';
}
function updateTplSyncIndicator() {
const el = document.getElementById('tpl-sync-indicator');
if (!el) return;
const dot = el.querySelector('.sync-dot');
const label = el.querySelector('span:last-child');
if (Object.keys(tplSyncedHashes).length === 0) {
dot.className = 'sync-dot unknown';
label.textContent = 'Sync-Status unbekannt';
return;
}
getTemplates().then(templates => {
const outOfSync = templates.filter(t => getTplSyncClass(t) === 'out-of-sync').length;
const unknown = templates.filter(t => getTplSyncClass(t) === 'unknown').length;
if (outOfSync > 0) {
dot.className = 'sync-dot out-of-sync';
label.textContent = `${outOfSync} Vorlage(n) nicht hochgeladen`;
} else if (unknown > 0) {
dot.className = 'sync-dot unknown';
label.textContent = `${unknown} Vorlage(n) unbekannt`;
} else {
dot.className = 'sync-dot in-sync';
label.textContent = 'Alle Vorlagen synchron';
}
});
}
function updateSigSyncIndicator() {
const el = document.getElementById('sig-sync-indicator');
if (!el) return;
const dot = el.querySelector('.sync-dot');
const label = el.querySelector('span:last-child');
if (Object.keys(sigSyncedHashes).length === 0) {
dot.className = 'sync-dot unknown';
label.textContent = 'Sync-Status unbekannt';
return;
}
let outOfSync = 0;
for (const identity of allIdentities) {
const email = identity.email.toLowerCase();
if (sigSyncedHashes[email] !== undefined) {
if (simpleHash(identity.signature || '') !== sigSyncedHashes[email]) {
outOfSync++;
}
}
}
if (outOfSync > 0) {
dot.className = 'sync-dot out-of-sync';
label.textContent = `${outOfSync} Signatur(en) nicht hochgeladen`;
} else {
dot.className = 'sync-dot in-sync';
label.textContent = 'Alle Signaturen synchron';
}
}
// ── Template List Rendering ──
function renderTemplates(templates) {
templateList.innerHTML = '';
if (templates.length === 0) {
templateList.appendChild(noTemplatesMessage);
noTemplatesMessage.style.display = 'block';
return;
}
noTemplatesMessage.style.display = 'none';
templates.forEach(template => {
const item = document.createElement('div');
item.className = 'template-item';
const syncClass = getTplSyncClass(template);
const scopeValue = template.folder === '_gemeinsam' ? 'shared'
: template.folder?.startsWith('_benutzer/') ? 'private' : 'department';
const badgeClass = scopeValue === 'shared' ? 'badge-shared'
: scopeValue === 'department' ? 'badge-dept' : 'badge-private';
const folderBadge = `<select class="scope-select ${badgeClass}" data-id="${template.id}">
<option value="private" ${scopeValue === 'private' ? 'selected' : ''}>Persönlich</option>
<option value="department" ${scopeValue === 'department' ? 'selected' : ''}>${document.getElementById('sync-department')?.value || 'Abteilung'}</option>
<option value="shared" ${scopeValue === 'shared' ? 'selected' : ''}>Alle</option>
</select>`;
const pushBtn = syncClass === 'out-of-sync'
? `<button data-id="${template.id}" class="push-btn" title="Diese Vorlage hochladen">⬆ Hochladen</button>`
: '';
const pullBtn = hasServerUpdate(template)
? `<button data-id="${template.id}" class="pull-btn" title="Neuere Version vom Server laden">⬇ Laden</button>`
: '';
item.innerHTML = `
<span class="sync-dot ${syncClass}" title="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}" aria-label="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}"></span>
<label class="template-label"><input type="checkbox" class="template-checkbox" data-id="${template.id}"><span class="template-name">${template.name}</span>${folderBadge}</label>
<div class="template-actions">
${pullBtn}${pushBtn}
<button data-id="${template.id}" class="edit-btn">Bearbeiten</button>
<button data-id="${template.id}" class="delete-btn">Löschen</button>
</div>
`;
templateList.appendChild(item);
});
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));
document.querySelectorAll('.scope-select').forEach(sel => sel.addEventListener('change', handleScopeChange));
}
// ── Inline Editor Panel ──
function openEditorPanel() {
editorPanel.classList.add('open');
// Update department name in scope dropdown
const deptName = document.getElementById('sync-department')?.value || 'Abteilung';
const deptOption = document.querySelector('#tpl-scope-select option[value="department"]');
if (deptOption) deptOption.textContent = deptName;
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-scope-select').value = 'private';
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();
if (!content.trim()) {
showToast('Bitte Inhalt eingeben.', 'error');
return;
}
let templates = await getTemplates();
const scope = document.getElementById('tpl-scope-select').value;
const authorEmail = await getAuthorEmail();
let folder;
if (scope === 'shared') folder = '_gemeinsam';
else if (scope === 'private') folder = `_benutzer/${authorEmail}`;
else folder = undefined; // department — pushTemplates sets this.department
if (scope === 'private' && !authorEmail) {
showToast('Bitte E-Mail in den Einstellungen eintragen für persönliche Vorlagen.', 'error');
return;
}
if (id) {
const index = templates.findIndex(t => t.id === id);
if (index > -1) {
const existingTemplate = templates[index];
const oldFolder = existingTemplate.folder;
const scopeChanged = oldFolder !== folder;
// Handle scope change: delete old remote file if needed
if (scopeChanged && existingTemplate.remotePath) {
const isDowngrade = (oldFolder === '_gemeinsam' && folder !== '_gemeinsam') ||
(!oldFolder?.startsWith('_benutzer/') && folder?.startsWith('_benutzer/'));
if (isDowngrade) {
const confirmed = await showConfirmDialog(
'Sichtbarkeit verringern?',
'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.',
'Trotzdem ändern'
);
if (!confirmed) return;
}
try {
await browser.runtime.sendMessage({
action: 'deleteRemoteTemplate',
remotePath: existingTemplate.remotePath
});
} catch (_) {}
existingTemplate.remotePath = undefined;
}
templates[index] = { id, name, content, folder, remotePath: existingTemplate.remotePath };
}
} else {
templates.push({ id: Date.now().toString(), name, content, folder });
}
await saveTemplates(templates);
renderTemplates(templates);
closeEditorPanel();
updateTplSyncIndicator();
// Auto-push to server
try {
await browser.runtime.sendMessage({ action: 'pushTemplates' });
const freshTemplates = await getTemplates();
await checkForServerUpdates();
storeTplHashes(freshTemplates);
renderTemplates(freshTemplates);
updateTplSyncIndicator();
} catch (_) {}
});
// ── 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;
const scopeSelect = document.getElementById('tpl-scope-select');
if (template.folder === '_gemeinsam') scopeSelect.value = 'shared';
else if (template.folder?.startsWith('_benutzer/')) scopeSelect.value = 'private';
else scopeSelect.value = 'department';
setEditorContent(template.content);
formLegend.textContent = 'Vorlage bearbeiten';
saveButton.textContent = 'Aktualisieren';
openEditorPanel();
}
async function handleDelete(e) {
const id = e.target.dataset.id;
let templates = await getTemplates();
const template = templates.find(t => t.id === id);
if (!template) return;
if (template.remotePath) {
const confirmed = await showConfirmDialog(
`"${template.name}" löschen?`,
'Die Vorlage wird lokal und vom Server gelöscht (für alle Nutzer).',
'Überall löschen'
);
if (!confirmed) return;
// Delete from server
try {
const result = await browser.runtime.sendMessage({
action: 'deleteRemoteTemplate',
remotePath: template.remotePath
});
if (!result?.success) {
showToast('Fehler beim Löschen vom Server: ' + (result?.error || 'Unbekannt'), 'error', 6000);
}
} catch (err) {
showToast('Fehler beim Löschen vom Server: ' + err.message, 'error', 6000);
}
} else {
const confirmed = await showConfirmDialog(
'Vorlage löschen?',
`"${template.name}" wirklich löschen?`
);
if (!confirmed) return;
}
templates = templates.filter(t => t.id !== id);
await saveTemplates(templates);
renderTemplates(templates);
updateTplSyncIndicator();
showToast('Vorlage gelöscht.', 'info');
}
async function handlePullSingle(e) {
const btn = e.target.closest('.pull-btn') || e.target;
const id = btn.dataset.id;
const templates = await getTemplates();
const template = templates.find(t => t.id === id);
if (!template || !template.remotePath) return;
if (!checkOnline()) return;
const origHTML = btn.innerHTML;
btn.innerHTML = '<span class="spinner"></span>';
btn.disabled = true;
try {
const result = await browser.runtime.sendMessage({
action: 'pullSingleTemplate',
remotePath: template.remotePath
});
if (result && result.success) {
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();
showToast('Vorlage heruntergeladen.', 'success');
} else {
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
btn.innerHTML = origHTML;
btn.disabled = false;
}
} catch (err) {
showToast('Fehler: ' + err.message, 'error', 6000);
btn.innerHTML = origHTML;
btn.disabled = false;
}
}
async function handlePushSingle(e) {
const btn = e.target.closest('.push-btn') || e.target;
const id = btn.dataset.id;
const templates = await getTemplates();
const template = templates.find(t => t.id === id);
if (!template) return;
if (!checkOnline()) return;
const origHTML = btn.innerHTML;
btn.innerHTML = '<span class="spinner"></span>';
btn.disabled = true;
try {
const result = await browser.runtime.sendMessage({
action: 'pushSingleTemplate',
templateId: id
});
if (result && result.success) {
tplSyncedHashes[id] = simpleHash(template.content || '');
saveSyncHashes();
renderTemplates(templates);
updateTplSyncIndicator();
showToast('Vorlage hochgeladen.', 'success');
} else {
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
btn.innerHTML = origHTML;
btn.disabled = false;
}
} catch (err) {
showToast('Fehler: ' + err.message, 'error', 6000);
btn.innerHTML = origHTML;
btn.disabled = false;
}
}
// ── Inline Scope Change ──
function folderToScope(folder) {
if (folder === '_gemeinsam') return 'shared';
if (folder?.startsWith('_benutzer/')) return 'private';
return 'department';
}
async function handleScopeChange(e) {
const id = e.target.dataset.id;
const newScope = e.target.value;
let templates = await getTemplates();
const template = templates.find(t => t.id === id);
if (!template) return;
const oldScope = folderToScope(template.folder);
if (oldScope === newScope) return;
const authorEmail = await getAuthorEmail();
if (newScope === 'private' && !authorEmail) {
showToast('Bitte E-Mail in den Einstellungen eintragen.', 'error');
e.target.value = oldScope;
return;
}
// Warn on downgrade
const scopeRank = { shared: 2, department: 1, private: 0 };
if (scopeRank[newScope] < scopeRank[oldScope] && template.remotePath) {
const confirmed = await showConfirmDialog(
'Sichtbarkeit verringern?',
'Die Vorlage wird am bisherigen Ort gelöscht. Andere Nutzer verlieren ggf. den Zugriff.',
'Trotzdem ändern'
);
if (!confirmed) {
e.target.value = oldScope;
return;
}
}
// Delete old remote file if exists
if (template.remotePath) {
try {
await browser.runtime.sendMessage({
action: 'deleteRemoteTemplate',
remotePath: template.remotePath
});
} catch (_) {}
template.remotePath = undefined;
}
// Set new folder
if (newScope === 'shared') template.folder = '_gemeinsam';
else if (newScope === 'private') template.folder = `_benutzer/${authorEmail}`;
else template.folder = undefined;
await saveTemplates(templates);
// Auto-push to new location
try {
await browser.runtime.sendMessage({ action: 'pushTemplates' });
templates = await getTemplates();
storeTplHashes(templates);
} catch (_) {}
renderTemplates(templates);
updateTplSyncIndicator();
showToast('Sichtbarkeit geändert.', 'success');
}
// ── Bulk Actions ──
document.getElementById('select-all-button').addEventListener('click', () => {
const checkboxes = document.querySelectorAll('.template-checkbox');
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
checkboxes.forEach(cb => cb.checked = !allChecked);
});
document.getElementById('delete-selected-button').addEventListener('click', async () => {
const checked = document.querySelectorAll('.template-checkbox:checked');
if (checked.length === 0) return;
const confirmed = await showConfirmDialog(
'Ausgewählte löschen?',
`${checked.length} Vorlage(n) wirklich löschen?`,
`${checked.length} löschen`
);
if (!confirmed) return;
const idsToDelete = new Set(Array.from(checked).map(cb => cb.dataset.id));
let templates = await getTemplates();
templates = templates.filter(t => !idsToDelete.has(t.id));
await saveTemplates(templates);
renderTemplates(templates);
showToast(`${checked.length} Vorlage(n) gelöscht.`, 'info');
});
// ── HTML File Import ──
document.getElementById('import-button').addEventListener('click', async () => {
const fileInput = document.getElementById('import-files');
const statusEl = document.getElementById('import-status');
const files = fileInput.files;
if (files.length === 0) {
showToast('Bitte Dateien auswählen!', 'error');
return;
}
let templates = await getTemplates();
let importCount = 0;
for (const file of files) {
const content = await file.text();
const name = file.name.replace(/\.html?$/i, '');
let body = content;
const bodyMatch = content.match(/<body[^>]*>([\s\S]*)<\/body>/i);
if (bodyMatch) body = bodyMatch[1].trim();
const existingIndex = templates.findIndex(t => t.name.toLowerCase() === name.toLowerCase());
if (existingIndex > -1) {
templates[existingIndex].content = body;
} else {
templates.push({ id: Date.now().toString() + importCount, name, content: body });
}
importCount++;
}
await saveTemplates(templates);
renderTemplates(templates);
updateTplSyncIndicator();
showToast(`${importCount} Vorlage(n) importiert!`, 'success');
fileInput.value = '';
});
// ── Sync "Aktualisieren" Button (Pull + Push) ──
document.getElementById('sync-refresh-button').addEventListener('click', async () => {
if (!checkOnline()) return;
const btn = document.getElementById('sync-refresh-button');
const origText = btn.textContent;
btn.disabled = true;
btn.innerHTML = '<span class="spinner spinner-dark"></span> Synchronisiere...';
try {
// Pull first
const pullResult = await browser.runtime.sendMessage({ action: 'pullTemplates' });
if (!pullResult?.success) {
showToast(pullResult?.error || 'Fehler beim Laden', 'error', 6000);
btn.disabled = false;
btn.textContent = origText;
return;
}
// Then push
const pushResult = await browser.runtime.sendMessage({ action: 'pushTemplates' });
const templates = await getTemplates();
await checkForServerUpdates();
storeTplHashes(templates);
renderTemplates(templates);
updateTplSyncIndicator();
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
} catch (err) {
showToast('Fehler: ' + err.message, 'error', 6000);
}
btn.disabled = false;
btn.textContent = origText;
});
// ── Signaturen ──
async function getSigSourceMap() {
const result = await browser.storage.local.get(SIG_SOURCE_KEY);
return result[SIG_SOURCE_KEY] || {};
}
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() {
allIdentities = [];
sigIdentitySelect.innerHTML = '';
const accounts = await browser.accounts.list();
for (const account of accounts) {
const identities = await browser.identities.list(account.id);
for (const identity of identities) {
const label = identity.name
? `${identity.name} <${identity.email}>`
: identity.email;
allIdentities.push({
id: identity.id,
email: identity.email,
label,
accountName: account.name,
signature: identity.signature || '',
signatureIsPlainText: identity.signatureIsPlainText || false
});
const opt = document.createElement('option');
opt.value = identity.id;
opt.textContent = `${label} (${account.name})`;
sigIdentitySelect.appendChild(opt);
}
}
// Auto-select first identity and load signature directly
if (allIdentities.length > 0) {
const first = allIdentities[0];
sigIdentitySelect.value = first.id;
const sourceMap = await getSigSourceMap();
const source = sourceMap[first.email.toLowerCase()] || 'own';
updateSigSourceDropdown(first.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(first.signature || '');
}
updateSigEditorState(source);
}
}
// Load signature header into editor when identity is selected
sigIdentitySelect.addEventListener('change', async () => {
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
if (identity) {
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 = '';
sigSourceSelect.innerHTML = '<option value="own">Eigene Signatur</option>';
sigSourceInfo.textContent = '';
}
});
sigSourceSelect.addEventListener('change', async () => {
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
if (!identity) return;
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 Header/Footer Helpers ──
// The footer separator comment in the combined signature
const FOOTER_SEPARATOR = '<!-- SIG_FOOTER_START -->';
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;
}
} 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) {
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
showToast(message, type, type === 'error' ? 6000 : 4000);
}
// Save signature to Thunderbird identity (header + footer)
document.getElementById('sig-save-button').addEventListener('click', async () => {
const identityId = sigIdentitySelect.value;
if (!identityId) {
showSigStatus('Bitte Identität auswählen.', 'red');
return;
}
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: fullSignature,
signatureIsPlainText: false
});
if (identity) identity.signature = fullSignature;
updateSigSyncIndicator();
// 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
document.getElementById('sig-import-file-btn').addEventListener('click', () => {
document.getElementById('sig-import-file').click();
});
document.getElementById('sig-import-file').addEventListener('change', async (e) => {
const files = Array.from(e.target.files);
if (files.length === 0) return;
const htmlFile = files.find(f => /\.html?$/i.test(f.name));
const imageFiles = files.filter(f => f.type.startsWith('image/'));
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(/<body[^>]*>([\s\S]*)<\/body>/i);
if (bodyMatch) html = bodyMatch[1].trim();
html = html.replace(/<img\s+([^>]*?)src=["']([^"']+)["']/gi, (match, before, src) => {
const filename = src.split('/').pop().split('\\').pop().toLowerCase();
if (imageMap[filename]) return `<img ${before}src="${imageMap[filename]}"`;
return match;
});
const unresolvedImages = (html.match(/<img[^>]+src=["'](?!data:)[^"']+["']/gi) || []).length;
sigEditorArea.innerHTML = html;
e.target.value = '';
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 - "Aktualisieren" (pull + push)
document.getElementById('sig-sync-refresh').addEventListener('click', async () => {
if (!checkOnline()) return;
const btn = document.getElementById('sig-sync-refresh');
const origText = btn.textContent;
btn.disabled = true;
btn.innerHTML = '<span class="spinner spinner-dark"></span> Synchronisiere...';
try {
// Pull first (gets footer + headers)
const pullResult = await browser.runtime.sendMessage({ action: 'pullSignatures' });
if (!pullResult?.success) {
showToast(pullResult?.error || 'Fehler', 'error', 6000);
btn.disabled = false;
btn.textContent = origText;
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'));
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
} catch (err) {
showToast('Fehler: ' + err.message, 'error', 6000);
}
btn.disabled = false;
btn.textContent = origText;
});
// ── Footer Editor ──
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 === '<br>')) {
try {
const result = await browser.runtime.sendMessage({ action: 'loadFooter' });
if (result && result.success && result.html) {
footerEditorArea.innerHTML = result.html;
}
} catch (_) {}
}
});
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');
}
});
// Save & push footer
document.getElementById('footer-save-button').addEventListener('click', async () => {
const html = footerEditorArea.innerHTML;
if (!html || html === '<br>') {
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');
}
});
// ── Sync Settings UI ──
async function populateEmailDropdown() {
const select = document.getElementById('sync-author-email');
const accounts = await browser.accounts.list();
const emails = new Set();
for (const account of accounts) {
for (const identity of account.identities) {
if (identity.email && !emails.has(identity.email)) {
emails.add(identity.email);
const opt = document.createElement('option');
opt.value = identity.email;
opt.textContent = identity.email;
select.appendChild(opt);
}
}
}
}
async function loadSyncConfig() {
try {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
let config = result[SYNC_CONFIG_KEY];
// First launch: load defaults from defaults.local.json if available
if (!config) {
try {
const res = await fetch(browser.runtime.getURL('defaults.local.json'));
if (res.ok) {
config = await res.json();
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
}
} catch (_) {}
}
if (config) {
document.getElementById('sync-url').value = config.baseUrl || '';
document.getElementById('sync-owner').value = config.owner || '';
document.getElementById('sync-repo').value = config.repo || '';
document.getElementById('sync-branch').value = config.branch || 'main';
document.getElementById('sync-token').value = config.token || '';
document.getElementById('sync-author-name').value = config.authorName || '';
document.getElementById('sync-author-email').value = config.authorEmail || '';
cachedAuthorEmail = config.authorEmail || '';
if (config.baseUrl && config.token) {
updateSyncStatus('connected', 'Verbunden');
}
if (config.department) {
const deptSelect = document.getElementById('sync-department');
const opt = document.createElement('option');
opt.value = config.department;
opt.textContent = config.department;
opt.selected = true;
deptSelect.appendChild(opt);
}
loadDepartments();
// Auto-detect department + author from server config
if (config.baseUrl && config.token) {
tryAutoDetect(config);
}
}
} catch (_) {}
}
async function tryAutoDetect(currentConfig) {
try {
const result = await browser.runtime.sendMessage({ action: 'autoDetect' });
if (!result?.success || !result.config) return;
// result.config = { "info@hotel.de": "Rezeption", ... }
const deptMap = result.config;
// Get all Thunderbird identities
const accounts = await browser.accounts.list();
const tbEmails = [];
for (const account of accounts) {
for (const identity of account.identities) {
tbEmails.push(identity.email.toLowerCase());
}
}
// Find matching department email
let detectedDept = null;
let personalEmail = null;
const matchedEmails = [];
const unmatchedEmails = [];
for (const email of tbEmails) {
if (deptMap[email]) {
detectedDept = deptMap[email];
matchedEmails.push(email);
} else {
unmatchedEmails.push(email);
}
}
// The personal email is the one NOT in the department map
if (unmatchedEmails.length > 0) {
personalEmail = unmatchedEmails[0];
}
let changed = false;
// Auto-set department if not already set
if (detectedDept && !currentConfig.department) {
const deptSelect = document.getElementById('sync-department');
// Add option if not exists
let found = false;
for (const opt of deptSelect.options) {
if (opt.value === detectedDept) { opt.selected = true; found = true; break; }
}
if (!found) {
const opt = document.createElement('option');
opt.value = detectedDept;
opt.textContent = detectedDept;
opt.selected = true;
deptSelect.appendChild(opt);
}
currentConfig.department = detectedDept;
changed = true;
}
// Auto-set author email if not already set
if (personalEmail && !currentConfig.authorEmail) {
document.getElementById('sync-author-email').value = personalEmail;
currentConfig.authorEmail = personalEmail;
cachedAuthorEmail = personalEmail;
changed = true;
}
// Auto-set author name from TB identity if not already set
if (personalEmail && !currentConfig.authorName) {
for (const account of accounts) {
for (const identity of account.identities) {
if (identity.email.toLowerCase() === personalEmail && identity.name) {
document.getElementById('sync-author-name').value = identity.name;
currentConfig.authorName = identity.name;
changed = true;
break;
}
}
if (changed && currentConfig.authorName) break;
}
}
if (changed) {
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: currentConfig });
showToast('Einstellungen automatisch erkannt.', 'info');
}
} catch (err) {
console.log('Auto-detect failed (optional):', err.message);
}
}
function getSyncConfigFromForm() {
return {
baseUrl: document.getElementById('sync-url').value.replace(/\/+$/, ''),
owner: document.getElementById('sync-owner').value.trim(),
repo: document.getElementById('sync-repo').value.trim(),
branch: document.getElementById('sync-branch').value.trim() || 'main',
token: document.getElementById('sync-token').value.trim(),
authorName: document.getElementById('sync-author-name').value.trim(),
authorEmail: document.getElementById('sync-author-email').value.trim(),
department: document.getElementById('sync-department').value
};
}
function updateSyncStatus(type, message) {
const bar = document.getElementById('sync-status-bar');
bar.className = 'sync-status ' + type;
bar.textContent = message;
}
function showSyncActionStatus(elId, message, color) {
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
showToast(message, type, type === 'error' ? 6000 : 4000);
}
function appendSyncLog(message) {
const log = document.getElementById('sync-log');
log.style.display = 'block';
const time = new Date().toLocaleTimeString('de-DE');
log.textContent += `[${time}] ${message}\n`;
log.scrollTop = log.scrollHeight;
}
async function loadDepartments() {
try {
const result = await browser.runtime.sendMessage({ action: 'listDepartments' });
if (result && result.success) {
const select = document.getElementById('sync-department');
// 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 = '<option value="">— Bitte wählen —</option>';
for (const dept of result.departments) {
const opt = document.createElement('option');
opt.value = dept;
opt.textContent = dept;
if (dept === savedDept) opt.selected = true;
select.appendChild(opt);
}
}
} catch (_) {}
}
document.getElementById('save-sync-config').addEventListener('click', async () => {
const config = getSyncConfigFromForm();
if (!config.baseUrl || !config.owner || !config.repo || !config.token) {
showSyncActionStatus('sync-action-status', 'Bitte alle Verbindungsfelder ausfüllen.', 'red');
return;
}
try {
const origin = new URL(config.baseUrl).origin + '/*';
const granted = await browser.permissions.request({ origins: [origin] });
if (!granted) {
showSyncActionStatus('sync-action-status', 'Berechtigung für Server abgelehnt.', 'red');
return;
}
} catch (err) {
showSyncActionStatus('sync-action-status', 'Ungültige URL.', 'red');
return;
}
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
updateSyncStatus('connected', 'Verbunden');
showSyncActionStatus('sync-action-status', 'Gespeichert!', 'green');
appendSyncLog('Verbindung konfiguriert.');
loadDepartments();
});
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 });
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();
renderTemplates(templates);
storeTplHashes(templates);
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 (_) {}
}
});
for (const id of ['sync-author-name', 'sync-author-email']) {
document.getElementById(id).addEventListener('change', async () => {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
const config = result[SYNC_CONFIG_KEY] || {};
config.authorName = document.getElementById('sync-author-name').value.trim();
config.authorEmail = document.getElementById('sync-author-email').value.trim();
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
});
}
document.getElementById('refresh-departments').addEventListener('click', loadDepartments);
document.getElementById('test-sync-connection').addEventListener('click', async () => {
if (!checkOnline()) return;
const btn = document.getElementById('test-sync-connection');
const origText = btn.textContent;
btn.disabled = true;
btn.innerHTML = '<span class="spinner spinner-dark"></span> Teste...';
try {
const result = await browser.runtime.sendMessage({ action: 'testConnection' });
if (result && result.success) {
updateSyncStatus('connected', 'Verbunden');
showToast('Verbindung erfolgreich!', 'success');
appendSyncLog('Verbindungstest erfolgreich.');
loadDepartments();
} else {
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
showToast(result?.error || 'Fehler', 'error', 6000);
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
}
} catch (err) {
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
showToast('Sync nicht verfügbar.', 'error', 6000);
}
btn.disabled = false;
btn.textContent = origText;
});
// ── Init ──
window.addEventListener('load', async () => {
await loadSyncHashes();
const templates = await getTemplates();
renderTemplates(templates);
updateTplSyncIndicator();
await populateEmailDropdown();
loadSyncConfig();
await loadIdentities();
checkForServerUpdates();
setInterval(checkForServerUpdates, 30000);
// Auto-refresh UI when background sync updates hashes (fires after templates + hashes are both written)
browser.storage.onChanged.addListener(async (changes, area) => {
if (area !== 'local') return;
if (changes[HASH_STORAGE_KEY]) {
await loadSyncHashes();
const templates = await getTemplates();
renderTemplates(templates);
updateTplSyncIndicator();
}
});
});