v2.2.0: 3-Stufen-Sichtbarkeit, UX-Verbesserungen, Auto-Erkennung
- 3 Sichtbarkeitsstufen für Vorlagen: Persönlich / Abteilung / Alle
- Persönliche Vorlagen werden in _benutzer/{email}/ synchronisiert
- Sichtbarkeit direkt in der Liste per Dropdown änderbar
- Warnung beim Verringern der Sichtbarkeit (Server-Löschung)
- Auto-Erkennung von Abteilung + E-Mail via _config/abteilungen.json
- Toast-Benachrichtigungen statt unsichtbare Status-Badges
- Lade-Spinner bei Sync-Operationen
- Sync-Dots mit Symbolen (nicht nur Farbe) für Barrierefreiheit
- Custom Delete-Modal statt browser confirm()
- Collapsible-Sections visuell als klickbar erkennbar
- Token-Feld mit Show/Hide-Toggle
- Inline-Validierung für Template-Namen
- Checkbox-Klickflächen vergrößert + Label-Klick
- Offline-Erkennung mit Banner
- Font-Dropdown Viewport-Fix
- Popup: Prefix-Dropdown verständlicher
- Signaturen: erste Identität automatisch ausgewählt
- README komplett neu geschrieben
This commit is contained in:
@@ -6,6 +6,112 @@ 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');
|
||||
@@ -62,7 +168,20 @@ function renderFontDropdown(filter) {
|
||||
});
|
||||
fontDropdown.appendChild(div);
|
||||
}
|
||||
fontDropdown.classList.toggle('open', matches.length > 0);
|
||||
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));
|
||||
@@ -158,7 +277,20 @@ function renderSigFontDropdown(filter) {
|
||||
});
|
||||
sigFontDropdown.appendChild(div);
|
||||
}
|
||||
sigFontDropdown.classList.toggle('open', matches.length > 0);
|
||||
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));
|
||||
@@ -374,19 +506,24 @@ function renderTemplates(templates) {
|
||||
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 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">↑</button>`
|
||||
? `<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">↓</button>`
|
||||
? `<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'}"></span>
|
||||
<input type="checkbox" class="template-checkbox" data-id="${template.id}">
|
||||
<span class="template-name">${template.name}${folderBadge}</span>
|
||||
<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>
|
||||
@@ -400,12 +537,17 @@ function renderTemplates(templates) {
|
||||
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' });
|
||||
}
|
||||
|
||||
@@ -420,7 +562,7 @@ function closeEditorPanel() {
|
||||
|
||||
document.getElementById('new-template-button').addEventListener('click', () => {
|
||||
closeEditorPanel();
|
||||
document.getElementById('tpl-shared-toggle').checked = false;
|
||||
document.getElementById('tpl-scope-select').value = 'private';
|
||||
formLegend.textContent = 'Neue Vorlage erstellen';
|
||||
saveButton.textContent = 'Speichern';
|
||||
openEditorPanel();
|
||||
@@ -437,30 +579,73 @@ templateForm.addEventListener('submit', async (e) => {
|
||||
const content = getEditorContent();
|
||||
|
||||
if (!content.trim()) {
|
||||
alert('Bitte Inhalt eingeben.');
|
||||
showToast('Bitte Inhalt eingeben.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let templates = await getTemplates();
|
||||
|
||||
const isShared = document.getElementById('tpl-shared-toggle').checked;
|
||||
const folder = isShared ? '_gemeinsam' : undefined;
|
||||
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 remotePath = templates[index].remotePath;
|
||||
const effectiveFolder = folder !== undefined ? folder : templates[index].folder;
|
||||
templates[index] = { id, name, content, folder: effectiveFolder, remotePath };
|
||||
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: folder || undefined });
|
||||
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 ──
|
||||
@@ -473,7 +658,10 @@ async function handleEdit(e) {
|
||||
|
||||
document.getElementById('template-id').value = template.id;
|
||||
document.getElementById('template-name').value = template.name;
|
||||
document.getElementById('tpl-shared-toggle').checked = (template.folder === '_gemeinsam');
|
||||
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';
|
||||
@@ -487,12 +675,12 @@ async function handleDelete(e) {
|
||||
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`
|
||||
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 (!choice) return;
|
||||
if (!confirmed) return;
|
||||
|
||||
// Delete from server
|
||||
try {
|
||||
@@ -501,29 +689,37 @@ async function handleDelete(e) {
|
||||
remotePath: template.remotePath
|
||||
});
|
||||
if (!result?.success) {
|
||||
alert('Fehler beim Löschen vom Server: ' + (result?.error || 'Unbekannt'));
|
||||
showToast('Fehler beim Löschen vom Server: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Fehler beim Löschen vom Server: ' + err.message);
|
||||
showToast('Fehler beim Löschen vom Server: ' + err.message, 'error', 6000);
|
||||
}
|
||||
} else {
|
||||
if (!confirm('Diese Vorlage wirklich löschen?')) return;
|
||||
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 id = e.target.dataset.id;
|
||||
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;
|
||||
|
||||
e.target.textContent = '...';
|
||||
e.target.disabled = true;
|
||||
const origHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="spinner"></span>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({
|
||||
@@ -540,26 +736,30 @@ async function handlePullSingle(e) {
|
||||
saveSyncHashes();
|
||||
renderTemplates(templates);
|
||||
updateTplSyncIndicator();
|
||||
showToast('Vorlage heruntergeladen.', 'success');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
e.target.textContent = '\u2193';
|
||||
e.target.disabled = false;
|
||||
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||||
btn.innerHTML = origHTML;
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Fehler: ' + err.message);
|
||||
e.target.textContent = '\u2193';
|
||||
e.target.disabled = false;
|
||||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||
btn.innerHTML = origHTML;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePushSingle(e) {
|
||||
const id = e.target.dataset.id;
|
||||
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;
|
||||
|
||||
e.target.textContent = '...';
|
||||
e.target.disabled = true;
|
||||
const origHTML = btn.innerHTML;
|
||||
btn.innerHTML = '<span class="spinner"></span>';
|
||||
btn.disabled = true;
|
||||
|
||||
try {
|
||||
const result = await browser.runtime.sendMessage({
|
||||
@@ -571,18 +771,87 @@ async function handlePushSingle(e) {
|
||||
saveSyncHashes();
|
||||
renderTemplates(templates);
|
||||
updateTplSyncIndicator();
|
||||
showToast('Vorlage hochgeladen.', 'success');
|
||||
} else {
|
||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
||||
e.target.textContent = '\u2191';
|
||||
e.target.disabled = false;
|
||||
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||||
btn.innerHTML = origHTML;
|
||||
btn.disabled = false;
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Fehler: ' + err.message);
|
||||
e.target.textContent = '\u2191';
|
||||
e.target.disabled = false;
|
||||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||
btn.innerHTML = origHTML;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Inline Scope Change ──
|
||||
|
||||
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 authorEmail = await getAuthorEmail();
|
||||
const oldFolder = template.folder;
|
||||
let newFolder;
|
||||
if (newScope === 'shared') newFolder = '_gemeinsam';
|
||||
else if (newScope === 'private') newFolder = `_benutzer/${authorEmail}`;
|
||||
else newFolder = undefined;
|
||||
|
||||
if (oldFolder === newFolder) return;
|
||||
|
||||
if (newScope === 'private' && !authorEmail) {
|
||||
showToast('Bitte E-Mail in den Einstellungen eintragen.', 'error');
|
||||
// Reset
|
||||
e.target.value = oldFolder === '_gemeinsam' ? 'shared'
|
||||
: oldFolder?.startsWith('_benutzer/') ? 'private' : 'department';
|
||||
return;
|
||||
}
|
||||
|
||||
// Warn on downgrade
|
||||
const isDowngrade = (oldFolder === '_gemeinsam' && newFolder !== '_gemeinsam') ||
|
||||
(!oldFolder?.startsWith('_benutzer/') && newFolder?.startsWith('_benutzer/'));
|
||||
|
||||
if (isDowngrade && 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 = oldFolder === '_gemeinsam' ? 'shared'
|
||||
: oldFolder?.startsWith('_benutzer/') ? 'private' : 'department';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete old remote file if exists
|
||||
if (template.remotePath) {
|
||||
try {
|
||||
await browser.runtime.sendMessage({
|
||||
action: 'deleteRemoteTemplate',
|
||||
remotePath: template.remotePath
|
||||
});
|
||||
} catch (_) {}
|
||||
template.remotePath = undefined;
|
||||
}
|
||||
|
||||
template.folder = newFolder;
|
||||
await saveTemplates(templates);
|
||||
|
||||
// Auto-push to new location
|
||||
try {
|
||||
await browser.runtime.sendMessage({ action: 'pushTemplates' });
|
||||
templates = await getTemplates();
|
||||
} catch (_) {}
|
||||
|
||||
renderTemplates(templates);
|
||||
updateTplSyncIndicator();
|
||||
showToast('Sichtbarkeit geändert.', 'success');
|
||||
}
|
||||
|
||||
// ── Bulk Actions ──
|
||||
|
||||
document.getElementById('select-all-button').addEventListener('click', () => {
|
||||
@@ -594,13 +863,19 @@ document.getElementById('select-all-button').addEventListener('click', () => {
|
||||
document.getElementById('delete-selected-button').addEventListener('click', async () => {
|
||||
const checked = document.querySelectorAll('.template-checkbox:checked');
|
||||
if (checked.length === 0) return;
|
||||
if (!confirm(`${checked.length} Vorlage(n) wirklich löschen?`)) 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 ──
|
||||
@@ -611,9 +886,7 @@ document.getElementById('import-button').addEventListener('click', async () => {
|
||||
const files = fileInput.files;
|
||||
|
||||
if (files.length === 0) {
|
||||
statusEl.textContent = 'Bitte Dateien auswählen!';
|
||||
statusEl.style.color = 'red';
|
||||
statusEl.style.display = 'inline';
|
||||
showToast('Bitte Dateien auswählen!', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -640,28 +913,26 @@ document.getElementById('import-button').addEventListener('click', async () => {
|
||||
renderTemplates(templates);
|
||||
updateTplSyncIndicator();
|
||||
|
||||
statusEl.textContent = `${importCount} Vorlage(n) importiert!`;
|
||||
statusEl.style.color = 'green';
|
||||
statusEl.style.display = 'inline';
|
||||
showToast(`${importCount} Vorlage(n) importiert!`, 'success');
|
||||
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';
|
||||
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) {
|
||||
statusEl.textContent = pullResult?.error || 'Fehler beim Laden';
|
||||
statusEl.style.color = 'red';
|
||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
||||
showToast(pullResult?.error || 'Fehler beim Laden', 'error', 6000);
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -674,14 +945,12 @@ document.getElementById('sync-refresh-button').addEventListener('click', async (
|
||||
renderTemplates(templates);
|
||||
updateTplSyncIndicator();
|
||||
|
||||
const msg = `${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`;
|
||||
statusEl.textContent = msg;
|
||||
statusEl.style.color = 'green';
|
||||
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
|
||||
} catch (err) {
|
||||
statusEl.textContent = 'Fehler: ' + err.message;
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||
}
|
||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
});
|
||||
|
||||
// ── Signaturen ──
|
||||
@@ -756,6 +1025,12 @@ async function loadIdentities() {
|
||||
sigIdentitySelect.appendChild(opt);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select first identity and trigger change
|
||||
if (allIdentities.length > 0 && !sigIdentitySelect.value) {
|
||||
sigIdentitySelect.value = allIdentities[0].id;
|
||||
sigIdentitySelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
}
|
||||
|
||||
// Load signature header into editor when identity is selected
|
||||
@@ -871,11 +1146,8 @@ document.getElementById('sig-load-template').addEventListener('click', async ()
|
||||
});
|
||||
|
||||
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);
|
||||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||
}
|
||||
|
||||
// Save signature to Thunderbird identity (header + footer)
|
||||
@@ -979,18 +1251,19 @@ document.getElementById('sig-import-file').addEventListener('change', async (e)
|
||||
|
||||
// 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';
|
||||
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) {
|
||||
statusEl.textContent = pullResult?.error || 'Fehler';
|
||||
statusEl.style.color = 'red';
|
||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
||||
showToast(pullResult?.error || 'Fehler', 'error', 6000);
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1005,14 +1278,12 @@ document.getElementById('sig-sync-refresh').addEventListener('click', async () =
|
||||
updateSigSyncIndicator();
|
||||
sigIdentitySelect.dispatchEvent(new Event('change'));
|
||||
|
||||
const msg = `${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`;
|
||||
statusEl.textContent = msg;
|
||||
statusEl.style.color = 'green';
|
||||
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
|
||||
} catch (err) {
|
||||
statusEl.textContent = 'Fehler: ' + err.message;
|
||||
statusEl.style.color = 'red';
|
||||
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||
}
|
||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
});
|
||||
|
||||
// ── Footer Editor ──
|
||||
@@ -1038,11 +1309,8 @@ document.getElementById('footer-toggle').addEventListener('click', async functio
|
||||
});
|
||||
|
||||
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);
|
||||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||
}
|
||||
|
||||
// Load footer from server
|
||||
@@ -1096,6 +1364,7 @@ async function loadSyncConfig() {
|
||||
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');
|
||||
}
|
||||
@@ -1108,10 +1377,105 @@ async function loadSyncConfig() {
|
||||
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(/\/+$/, ''),
|
||||
@@ -1132,11 +1496,8 @@ function updateSyncStatus(type, 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);
|
||||
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||
}
|
||||
|
||||
function appendSyncLog(message) {
|
||||
@@ -1232,23 +1593,30 @@ for (const id of ['sync-author-name', 'sync-author-email']) {
|
||||
document.getElementById('refresh-departments').addEventListener('click', loadDepartments);
|
||||
|
||||
document.getElementById('test-sync-connection').addEventListener('click', async () => {
|
||||
showSyncActionStatus('sync-action-status', 'Teste...', '#777');
|
||||
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');
|
||||
showSyncActionStatus('sync-action-status', 'Verbindung erfolgreich!', 'green');
|
||||
showToast('Verbindung erfolgreich!', 'success');
|
||||
appendSyncLog('Verbindungstest erfolgreich.');
|
||||
loadDepartments();
|
||||
} else {
|
||||
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
||||
showSyncActionStatus('sync-action-status', result?.error || 'Fehler', 'red');
|
||||
showToast(result?.error || 'Fehler', 'error', 6000);
|
||||
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
|
||||
}
|
||||
} catch (err) {
|
||||
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
||||
showSyncActionStatus('sync-action-status', 'Sync nicht verfügbar.', 'red');
|
||||
showToast('Sync nicht verfügbar.', 'error', 6000);
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
});
|
||||
|
||||
// ── Init ──
|
||||
|
||||
Reference in New Issue
Block a user