Files
hps-thunderbird-templates/templates_options/templates_options.js
Kendrick Bollens cf051458bb Feature: WYSIWYG-Editor, Gitea-Sync, Signaturen-Verwaltung
- WYSIWYG-Editor mit contenteditable statt Textarea (MDI-Icons, System-Fonts, Farbwähler)
- Gitea-Sync: Templates per Abteilung aus Git-Repo laden/hochladen mit Commit-Author
- Abteilungsordner + _gemeinsam Ordner, einzelnes Pull/Push pro Vorlage
- Sync-Status pro Vorlage (grün/rot/grau Ampel), persistent über Neustarts
- Signaturen-Tab: Identitäten bearbeiten, aus Datei laden, Sync über signatures/ Ordner
- Persönliche Signaturen für geteilte E-Mail-Adressen (pro Mitarbeiter)
- Tab-Navigation: Vorlagen, Signaturen, Synchronisierung
- Auto-Pull beim Thunderbird-Start (Templates + Signaturen)
2026-04-20 16:30:40 +02:00

981 lines
36 KiB
JavaScript

// templates_options/templates_options.js
const TEMPLATE_STORAGE_KEY = 'message_templates';
// DOM elements
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');
// ── 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'
];
// Build list of available system fonts
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(); // prevent blur before click fires
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', () => {
// Small delay so mousedown on option fires first
setTimeout(() => fontDropdown.classList.remove('open'), 150);
});
// Apply custom font on Enter
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);
}
});
// ── Contenteditable Editor ──
function getEditorContent() {
const html = editorArea.innerHTML;
return (!html || html === '<br>') ? '' : html;
}
function setEditorContent(html) {
editorArea.innerHTML = html || '';
}
// ── Toolbar Commands ──
document.querySelectorAll('.editor-toolbar button[data-cmd]').forEach(btn => {
btn.addEventListener('click', () => {
editorArea.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', () => {
editorArea.focus();
document.execCommand(cmd, false, colorInput.value);
});
colorInput.click();
return;
}
}
document.execCommand(cmd, false, val);
});
});
// Font size dropdown
document.querySelector('.editor-toolbar select[data-cmd="fontSize"]').addEventListener('change', function() {
if (!this.value) return;
editorArea.focus();
document.execCommand('fontSize', false, this.value);
this.value = '';
});
// ── 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');
});
});
// ── 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) {
try {
await browser.storage.local.set({ [TEMPLATE_STORAGE_KEY]: templates });
} catch (error) {
console.error("Error saving templates:", error);
}
}
// ── Sync Status Tracking ──
let tplSyncedHashes = {}; // template id -> hash of content after last pull/push
let sigSyncedHashes = {}; // email -> hash of signature after last pull/push
const HASH_STORAGE_KEY = 'sync_hashes';
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 || {};
} catch (_) {}
}
async function saveSyncHashes() {
await browser.storage.local.set({
[HASH_STORAGE_KEY]: { tpl: tplSyncedHashes, sig: sigSyncedHashes }
});
}
function storeTplHashes(templates) {
tplSyncedHashes = {};
for (const t of templates) {
tplSyncedHashes[t.id] = simpleHash(t.content || '');
}
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() {
// Update the global indicator based on all templates
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">&#8593;</button>`
: '';
const pullBtn = template.folder
? `<button data-id="${template.id}" class="pull-btn" title="Diese Vorlage vom Server laden">&#8595;</button>`
: '';
item.innerHTML = `
<span class="sync-dot ${syncClass}" title="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}" style="flex-shrink:0;"></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(button => {
button.addEventListener('click', handleEdit);
});
document.querySelectorAll('.delete-btn').forEach(button => {
button.addEventListener('click', handleDelete);
});
document.querySelectorAll('.push-btn').forEach(button => {
button.addEventListener('click', handlePushSingle);
});
document.querySelectorAll('.pull-btn').forEach(button => {
button.addEventListener('click', handlePullSingle);
});
}
// ── 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();
if (id) {
const index = templates.findIndex(t => t.id === id);
if (index > -1) {
// Preserve folder info from synced templates
const folder = templates[index].folder;
const remotePath = templates[index].remotePath;
templates[index] = { id, name, content, folder, remotePath };
}
} else {
const newId = Date.now().toString();
templates.push({ id: newId, name, content });
}
await saveTemplates(templates);
renderTemplates(templates);
resetForm();
updateTplSyncIndicator();
});
// ── Edit / Delete ──
async function handleDelete(e) {
if (!confirm('Diese Vorlage wirklich löschen?')) return;
const idToDelete = e.target.dataset.id;
let templates = await getTemplates();
templates = templates.filter(t => t.id !== idToDelete);
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) {
// Update local template with pulled content
template.content = result.content;
await saveTemplates(templates);
tplSyncedHashes[id] = simpleHash(result.content || '');
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;
}
}
async function handleEdit(e) {
const idToEdit = e.target.dataset.id;
const templates = await getTemplates();
const template = templates.find(t => t.id === idToEdit);
if (template) {
document.getElementById('template-id').value = template.id;
document.getElementById('template-name').value = template.name;
setEditorContent(template.content);
formLegend.textContent = 'Vorlage bearbeiten';
saveButton.textContent = 'Aktualisieren';
cancelButton.style.display = 'inline';
window.scrollTo(0, 0);
}
}
function resetForm() {
templateForm.reset();
document.getElementById('template-id').value = '';
setEditorContent('');
formLegend.textContent = 'Neue Vorlage erstellen';
saveButton.textContent = 'Speichern';
cancelButton.style.display = 'none';
}
cancelButton.addEventListener('click', resetForm);
// ── Bulk Actions ──
document.getElementById('select-all-button').addEventListener('click', () => {
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} Template(s) 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: 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);
});
// ── Signaturen ──
const sigIdentitySelect = document.getElementById('sig-identity-select');
const sigEditorArea = document.getElementById('sig-editor-area');
const sigPersonalToggle = document.getElementById('sig-personal-toggle');
const sigPersonalInfo = document.getElementById('sig-personal-info');
let allIdentities = [];
// Persistent setting: which identities use personal signatures
const SIG_PERSONAL_KEY = 'sig_personal_emails';
async function getPersonalEmails() {
const result = await browser.storage.local.get(SIG_PERSONAL_KEY);
return result[SIG_PERSONAL_KEY] || [];
}
async function setPersonalEmail(email, enabled) {
const list = await getPersonalEmails();
const set = new Set(list);
if (enabled) set.add(email.toLowerCase());
else set.delete(email.toLowerCase());
await browser.storage.local.set({ [SIG_PERSONAL_KEY]: [...set] });
}
async function 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: 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 into editor when identity is selected
sigIdentitySelect.addEventListener('change', async () => {
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
if (identity) {
sigEditorArea.innerHTML = identity.signature || '';
const personalEmails = await getPersonalEmails();
const isPersonal = personalEmails.includes(identity.email.toLowerCase());
sigPersonalToggle.checked = isPersonal;
updatePersonalInfo(identity.email, isPersonal);
} else {
sigEditorArea.innerHTML = '';
sigPersonalToggle.checked = false;
sigPersonalInfo.textContent = '';
}
});
function updatePersonalInfo(email, isPersonal) {
const configName = document.getElementById('sync-author-name')?.value || '';
if (isPersonal && configName) {
sigPersonalInfo.textContent = `(${email} → persönlich für ${configName})`;
} else if (isPersonal) {
sigPersonalInfo.textContent = '(Name in Sync-Einstellungen eintragen!)';
} else {
sigPersonalInfo.textContent = '(gemeinsame Signatur für alle)';
}
}
// Toggle personal signature
sigPersonalToggle.addEventListener('change', async () => {
const identity = allIdentities.find(i => i.id === sigIdentitySelect.value);
if (!identity) return;
await setPersonalEmail(identity.email, sigPersonalToggle.checked);
updatePersonalInfo(identity.email, sigPersonalToggle.checked);
});
// Signature toolbar commands
document.querySelectorAll('#sig-toolbar button[data-cmd]').forEach(btn => {
btn.addEventListener('click', () => {
sigEditorArea.focus();
const cmd = btn.dataset.cmd;
let val = btn.dataset.val || null;
if (val === 'ask') {
if (cmd === 'createLink') {
val = prompt('Link-URL eingeben:', 'https://');
if (!val) return;
} else if (cmd === 'foreColor' || cmd === 'hiliteColor') {
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = cmd === 'foreColor' ? '#000000' : '#ffff00';
colorInput.addEventListener('input', () => {
sigEditorArea.focus();
document.execCommand(cmd, false, colorInput.value);
});
colorInput.click();
return;
}
}
document.execCommand(cmd, false, val);
});
});
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
document.getElementById('sig-save-button').addEventListener('click', async () => {
const identityId = sigIdentitySelect.value;
if (!identityId) {
showSigStatus('Bitte Identität auswählen.', 'red');
return;
}
const html = sigEditorArea.innerHTML;
await browser.identities.update(identityId, {
signature: html,
signatureIsPlainText: false
});
// Update local cache
const identity = allIdentities.find(i => i.id === identityId);
if (identity) identity.signature = html;
updateSigSyncIndicator();
showSigStatus('Signatur gespeichert!', 'green');
});
// 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 file = e.target.files[0];
if (!file) return;
const content = await file.text();
let body = content;
const bodyMatch = content.match(/<body[^>]*>([\s\S]*)<\/body>/i);
if (bodyMatch) body = bodyMatch[1].trim();
sigEditorArea.innerHTML = body;
e.target.value = '';
showSigStatus('Datei geladen — jetzt "Signatur speichern" klicken.', '#555');
});
// Signature sync - pull
document.getElementById('sig-sync-pull').addEventListener('click', async () => {
const statusEl = document.getElementById('sig-sync-status');
statusEl.textContent = 'Lade...';
statusEl.style.color = '#777';
statusEl.style.display = 'inline';
try {
const result = await browser.runtime.sendMessage({ action: 'pullSignatures' });
if (result && result.success) {
statusEl.textContent = `${result.updated || 0} Signatur(en) geladen!`;
statusEl.style.color = 'green';
await loadIdentities();
for (const id of allIdentities) {
sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
}
saveSyncHashes();
updateSigSyncIndicator();
sigIdentitySelect.dispatchEvent(new Event('change'));
} else {
statusEl.textContent = result?.error || 'Fehler';
statusEl.style.color = 'red';
}
} catch (err) {
statusEl.textContent = 'Fehler: ' + err.message;
statusEl.style.color = 'red';
}
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
});
// Signature sync - push
document.getElementById('sig-sync-push').addEventListener('click', async () => {
const statusEl = document.getElementById('sig-sync-status');
statusEl.textContent = 'Lade hoch...';
statusEl.style.color = '#777';
statusEl.style.display = 'inline';
try {
const result = await browser.runtime.sendMessage({ action: 'pushSignatures' });
if (result && result.success) {
statusEl.textContent = `${result.pushed || 0} Signatur(en) hochgeladen!`;
statusEl.style.color = 'green';
for (const id of allIdentities) {
sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || '');
}
saveSyncHashes();
updateSigSyncIndicator();
} else {
statusEl.textContent = result?.error || 'Fehler';
statusEl.style.color = 'red';
}
} catch (err) {
statusEl.textContent = 'Fehler: ' + err.message;
statusEl.style.color = 'red';
}
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
});
// ── Sync Settings UI ──
const SYNC_CONFIG_KEY = 'gitea_config';
async function loadSyncConfig() {
try {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
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');
}
// Load department after a short delay to ensure config is available
if (config.department) {
const deptSelect = document.getElementById('sync-department');
// Add saved department as option in case list hasn't loaded yet
const opt = document.createElement('option');
opt.value = config.department;
opt.textContent = config.department;
opt.selected = true;
deptSelect.appendChild(opt);
}
// Try to load department list
loadDepartments();
}
} catch (_) {}
}
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');
const currentVal = 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 === currentVal) opt.selected = true;
select.appendChild(opt);
}
}
} catch (_) {}
}
// Save sync config (connection + user info)
document.getElementById('save-sync-config').addEventListener('click', async () => {
const config = getSyncConfigFromForm();
if (!config.baseUrl || !config.owner || !config.repo || !config.token) {
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.');
// Load departments after saving
loadDepartments();
});
// Save department selection immediately when changed
document.getElementById('sync-department').addEventListener('change', async () => {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
const config = result[SYNC_CONFIG_KEY] || {};
config.department = document.getElementById('sync-department').value;
await browser.storage.local.set({ [SYNC_CONFIG_KEY]: config });
// Auto-pull templates for the new department
if (config.department) {
try {
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.`);
}
} catch (_) {}
}
});
// Save author name/email immediately when changed
for (const id of ['sync-author-name', 'sync-author-email']) {
document.getElementById(id).addEventListener('change', async () => {
const result = await browser.storage.local.get(SYNC_CONFIG_KEY);
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 });
});
}
// Refresh departments button
document.getElementById('refresh-departments').addEventListener('click', loadDepartments);
// Test connection
document.getElementById('test-sync-connection').addEventListener('click', async () => {
showSyncActionStatus('sync-action-status', 'Teste...', '#777');
try {
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');
}
});
// Pull (Vorlagen vom Server laden)
document.getElementById('sync-pull-button').addEventListener('click', async () => {
showSyncActionStatus('sync-sync-status', 'Lade Vorlagen...', '#777');
appendSyncLog('Vorlagen werden vom Server geladen...');
try {
const result = await browser.runtime.sendMessage({ action: 'pullTemplates' });
if (result && result.success) {
showSyncActionStatus('sync-sync-status', `${result.updated || 0} Vorlage(n) geladen!`, 'green');
appendSyncLog(`Pull abgeschlossen: ${result.updated || 0} Vorlage(n).`);
const templates = await getTemplates();
renderTemplates(templates);
storeTplHashes(templates);
updateTplSyncIndicator();
} else {
showSyncActionStatus('sync-sync-status', result?.error || 'Fehler', 'red');
appendSyncLog('Pull fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
}
} catch (err) {
showSyncActionStatus('sync-sync-status', 'Fehler: ' + err.message, 'red');
appendSyncLog('Fehler: ' + err.message);
}
});
// Push (Änderungen hochladen — nur per Knopfdruck)
document.getElementById('sync-push-button').addEventListener('click', async () => {
showSyncActionStatus('sync-sync-status', 'Lade hoch...', '#777');
appendSyncLog('Änderungen werden hochgeladen...');
try {
const result = await browser.runtime.sendMessage({ action: 'pushTemplates' });
if (result && result.success) {
showSyncActionStatus('sync-sync-status', `${result.pushed || 0} Änderung(en) hochgeladen!`, 'green');
appendSyncLog(`Push abgeschlossen: ${result.pushed || 0} Änderung(en).`);
const templates = await getTemplates();
renderTemplates(templates);
storeTplHashes(templates);
updateTplSyncIndicator();
} else {
showSyncActionStatus('sync-sync-status', result?.error || 'Fehler', 'red');
appendSyncLog('Push fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
}
} catch (err) {
showSyncActionStatus('sync-sync-status', 'Fehler: ' + err.message, 'red');
appendSyncLog('Fehler: ' + err.message);
}
});
// ── Init ──
window.addEventListener('load', async () => {
await loadSyncHashes();
const templates = await getTemplates();
renderTemplates(templates);
updateTplSyncIndicator();
loadSyncConfig();
loadIdentities();
});