- 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)
1267 lines
46 KiB
JavaScript
1267 lines
46 KiB
JavaScript
// 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 ──
|
|
|
|
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);
|
|
}
|
|
fontDropdown.classList.toggle('open', matches.length > 0);
|
|
}
|
|
|
|
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);
|
|
}
|
|
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);
|
|
}
|
|
});
|
|
|
|
// ── 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 folderBadge = template.folder
|
|
? `<span style="font-size:11px;color:#999;margin-left:6px;">[${template.folder}]</span>`
|
|
: '';
|
|
const pushBtn = syncClass === 'out-of-sync'
|
|
? `<button data-id="${template.id}" class="push-btn" title="Diese Vorlage hochladen">↑</button>`
|
|
: '';
|
|
const pullBtn = hasServerUpdate(template)
|
|
? `<button data-id="${template.id}" class="pull-btn" title="Neuere Version vom Server">↓</button>`
|
|
: '';
|
|
item.innerHTML = `
|
|
<span class="sync-dot ${syncClass}" title="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}"></span>
|
|
<input type="checkbox" class="template-checkbox" data-id="${template.id}">
|
|
<span class="template-name">${template.name}${folderBadge}</span>
|
|
<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));
|
|
}
|
|
|
|
// ── 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();
|
|
|
|
if (!content.trim()) {
|
|
alert('Bitte Inhalt eingeben.');
|
|
return;
|
|
}
|
|
|
|
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) {
|
|
const remotePath = templates[index].remotePath;
|
|
const effectiveFolder = folder !== undefined ? folder : templates[index].folder;
|
|
templates[index] = { id, name, content, folder: effectiveFolder, remotePath };
|
|
}
|
|
} else {
|
|
templates.push({ id: Date.now().toString(), name, content, folder: folder || undefined });
|
|
}
|
|
|
|
await saveTemplates(templates);
|
|
renderTemplates(templates);
|
|
closeEditorPanel();
|
|
updateTplSyncIndicator();
|
|
});
|
|
|
|
// ── 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) {
|
|
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 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();
|
|
}
|
|
|
|
async function handlePullSingle(e) {
|
|
const id = e.target.dataset.id;
|
|
const templates = await getTemplates();
|
|
const template = templates.find(t => t.id === id);
|
|
if (!template || !template.remotePath) return;
|
|
|
|
e.target.textContent = '...';
|
|
e.target.disabled = true;
|
|
|
|
try {
|
|
const result = await browser.runtime.sendMessage({
|
|
action: 'pullSingleTemplate',
|
|
remotePath: template.remotePath
|
|
});
|
|
if (result && result.success) {
|
|
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();
|
|
} else {
|
|
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
|
e.target.textContent = '\u2193';
|
|
e.target.disabled = false;
|
|
}
|
|
} catch (err) {
|
|
alert('Fehler: ' + err.message);
|
|
e.target.textContent = '\u2193';
|
|
e.target.disabled = false;
|
|
}
|
|
}
|
|
|
|
async function handlePushSingle(e) {
|
|
const id = e.target.dataset.id;
|
|
const templates = await getTemplates();
|
|
const template = templates.find(t => t.id === id);
|
|
if (!template) return;
|
|
|
|
e.target.textContent = '...';
|
|
e.target.disabled = true;
|
|
|
|
try {
|
|
const result = await browser.runtime.sendMessage({
|
|
action: 'pushSingleTemplate',
|
|
templateId: id
|
|
});
|
|
if (result && result.success) {
|
|
tplSyncedHashes[id] = simpleHash(template.content || '');
|
|
saveSyncHashes();
|
|
renderTemplates(templates);
|
|
updateTplSyncIndicator();
|
|
} else {
|
|
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
|
e.target.textContent = '\u2191';
|
|
e.target.disabled = false;
|
|
}
|
|
} catch (err) {
|
|
alert('Fehler: ' + err.message);
|
|
e.target.textContent = '\u2191';
|
|
e.target.disabled = false;
|
|
}
|
|
}
|
|
|
|
// ── Bulk Actions ──
|
|
|
|
document.getElementById('select-all-button').addEventListener('click', () => {
|
|
const checkboxes = document.querySelectorAll('.template-checkbox');
|
|
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
|
checkboxes.forEach(cb => cb.checked = !allChecked);
|
|
});
|
|
|
|
document.getElementById('delete-selected-button').addEventListener('click', async () => {
|
|
const checked = document.querySelectorAll('.template-checkbox:checked');
|
|
if (checked.length === 0) return;
|
|
if (!confirm(`${checked.length} Vorlage(n) wirklich löschen?`)) return;
|
|
|
|
const idsToDelete = new Set(Array.from(checked).map(cb => cb.dataset.id));
|
|
let templates = await getTemplates();
|
|
templates = templates.filter(t => !idsToDelete.has(t.id));
|
|
await saveTemplates(templates);
|
|
renderTemplates(templates);
|
|
});
|
|
|
|
// ── HTML File Import ──
|
|
|
|
document.getElementById('import-button').addEventListener('click', async () => {
|
|
const fileInput = document.getElementById('import-files');
|
|
const statusEl = document.getElementById('import-status');
|
|
const files = fileInput.files;
|
|
|
|
if (files.length === 0) {
|
|
statusEl.textContent = 'Bitte Dateien auswählen!';
|
|
statusEl.style.color = 'red';
|
|
statusEl.style.display = 'inline';
|
|
return;
|
|
}
|
|
|
|
let templates = await getTemplates();
|
|
let importCount = 0;
|
|
|
|
for (const file of files) {
|
|
const content = await file.text();
|
|
const name = file.name.replace(/\.html?$/i, '');
|
|
let body = content;
|
|
const bodyMatch = content.match(/<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();
|
|
|
|
statusEl.textContent = `${importCount} Vorlage(n) importiert!`;
|
|
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 ──
|
|
|
|
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 = '<option value="">— Bitte wählen —</option>';
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 el = document.getElementById('sig-status');
|
|
el.textContent = message;
|
|
el.style.color = color;
|
|
el.style.display = 'inline';
|
|
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
|
}
|
|
|
|
// Save signature to Thunderbird identity (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 () => {
|
|
const statusEl = document.getElementById('sig-sync-status');
|
|
statusEl.textContent = 'Synchronisiere...';
|
|
statusEl.style.color = '#777';
|
|
statusEl.style.display = 'inline';
|
|
|
|
try {
|
|
// 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';
|
|
}
|
|
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
|
});
|
|
|
|
// ── 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 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 {
|
|
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 loadSyncConfig() {
|
|
try {
|
|
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
|
|
const config = result[SYNC_CONFIG_KEY];
|
|
if (config) {
|
|
document.getElementById('sync-url').value = config.baseUrl || '';
|
|
document.getElementById('sync-owner').value = config.owner || '';
|
|
document.getElementById('sync-repo').value = config.repo || '';
|
|
document.getElementById('sync-branch').value = config.branch || 'main';
|
|
document.getElementById('sync-token').value = config.token || '';
|
|
document.getElementById('sync-author-name').value = config.authorName || '';
|
|
document.getElementById('sync-author-email').value = config.authorEmail || '';
|
|
if (config.baseUrl && config.token) {
|
|
updateSyncStatus('connected', 'Verbunden');
|
|
}
|
|
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();
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
function getSyncConfigFromForm() {
|
|
return {
|
|
baseUrl: document.getElementById('sync-url').value.replace(/\/+$/, ''),
|
|
owner: document.getElementById('sync-owner').value.trim(),
|
|
repo: document.getElementById('sync-repo').value.trim(),
|
|
branch: document.getElementById('sync-branch').value.trim() || 'main',
|
|
token: document.getElementById('sync-token').value.trim(),
|
|
authorName: document.getElementById('sync-author-name').value.trim(),
|
|
authorEmail: document.getElementById('sync-author-email').value.trim(),
|
|
department: document.getElementById('sync-department').value
|
|
};
|
|
}
|
|
|
|
function updateSyncStatus(type, message) {
|
|
const bar = document.getElementById('sync-status-bar');
|
|
bar.className = 'sync-status ' + type;
|
|
bar.textContent = message;
|
|
}
|
|
|
|
function showSyncActionStatus(elId, message, color) {
|
|
const el = document.getElementById(elId);
|
|
el.textContent = message;
|
|
el.style.color = color;
|
|
el.style.display = 'inline';
|
|
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
|
}
|
|
|
|
function appendSyncLog(message) {
|
|
const log = document.getElementById('sync-log');
|
|
log.style.display = 'block';
|
|
const time = new Date().toLocaleTimeString('de-DE');
|
|
log.textContent += `[${time}] ${message}\n`;
|
|
log.scrollTop = log.scrollHeight;
|
|
}
|
|
|
|
async function loadDepartments() {
|
|
try {
|
|
const result = await browser.runtime.sendMessage({ action: 'listDepartments' });
|
|
if (result && result.success) {
|
|
const select = document.getElementById('sync-department');
|
|
// 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 () => {
|
|
showSyncActionStatus('sync-action-status', 'Teste...', '#777');
|
|
try {
|
|
const result = await browser.runtime.sendMessage({ action: 'testConnection' });
|
|
if (result && result.success) {
|
|
updateSyncStatus('connected', 'Verbunden');
|
|
showSyncActionStatus('sync-action-status', 'Verbindung erfolgreich!', 'green');
|
|
appendSyncLog('Verbindungstest erfolgreich.');
|
|
loadDepartments();
|
|
} else {
|
|
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
|
showSyncActionStatus('sync-action-status', result?.error || 'Fehler', 'red');
|
|
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
|
|
}
|
|
} catch (err) {
|
|
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
|
showSyncActionStatus('sync-action-status', 'Sync nicht verfügbar.', 'red');
|
|
}
|
|
});
|
|
|
|
// ── Init ──
|
|
|
|
window.addEventListener('load', async () => {
|
|
await loadSyncHashes();
|
|
const templates = await getTemplates();
|
|
renderTemplates(templates);
|
|
updateTplSyncIndicator();
|
|
loadSyncConfig();
|
|
loadIdentities();
|
|
|
|
checkForServerUpdates();
|
|
setInterval(checkForServerUpdates, 30000);
|
|
});
|