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:
Kendrick Bollens
2026-05-06 22:04:46 +02:00
parent 33eb87613e
commit 864be54646
8 changed files with 861 additions and 160 deletions

110
README.md
View File

@@ -1,38 +1,92 @@
# Templates Reply
## Mozilla Thunderbird Add-On
# HPS Vorlagen & Signaturen
Templates Reply is a completely free and open-source Thunderbird extension that helps you create, manage, and reuse message templates directly from your compose window.
It is designed for speed, simplicity, and convenience — no sign-ups, no limits, and absolutely no data collection.
Thunderbird-Plugin (v2.2.0) zur zentralen Verwaltung von E-Mail-Vorlagen und Signaturen für Hotel Park Soltau. Vorlagen und Signaturen werden über ein Gitea/Forgejo-Repository synchronisiert und stehen so allen Mitarbeitern zur Verfügung.
This add-on does not track, store, or transmit any personal information.
All templates and settings are saved locally on your device and never leave your computer.
No analytics, telemetry, or remote servers are involved — your privacy is 100% respected.
## Features
You can freely use, modify, and distribute the source code under the terms of the MIT License.
## Screenshots
- **E-Mail-Vorlagen** erstellen, bearbeiten und per Klick in Compose-Fenster einfügen
- **3 Sichtbarkeitsstufen** pro Vorlage:
- **Persönlich** — nur für den eigenen Account, gesynct in `_benutzer/{email}/`
- **Abteilung** — für alle in der Abteilung, gesynct in den Abteilungsordner
- **Alle Abteilungen** — firmenweit, gesynct in `_gemeinsam/`
- **Signaturen-Verwaltung** mit persönlichem Kopfbereich + gemeinsamer Fußzeile pro Abteilung
- **Git-Sync** über Gitea/Forgejo API (Pull + Push, automatisch alle 15 Min.)
- **Auto-Erkennung** von Abteilung und Benutzer via `_config/abteilungen.json`
- **WYSIWYG-Editor** mit Schriftart, Farben, Listen, Bildern, Links
- **Sichtbarkeit direkt änderbar** per klickbarem Badge in der Vorlagenliste
![App Screenshot](https://addons.thunderbird.net/user-media/previews/full/207/207836.png?modified=1762427184)
![App Screenshot](https://addons.thunderbird.net/user-media/previews/full/207/207837.png?modified=1762427184)
![App Screenshot](https://addons.thunderbird.net/user-media/previews/full/207/207838.png?modified=1762427184)
## Repository-Struktur (Gitea)
## Install Locally (Temporary / Developer Mode)
```
Open Thunderbird
Go to Tools → Add-ons and Themes
Click the ⚙️ (gear icon) in the top-right corner
Select “Debug Add-ons”
Click “Load Temporary Add-on…”
Choose the manifest.json file from your add-on directory
⚠️ Note: This installation is temporary and will be removed when Thunderbird is restarted.
repo/
├── _gemeinsam/ # Vorlagen für alle Abteilungen
│ └── beispiel-vorlage.html
├── _benutzer/ # Persönliche Vorlagen pro User
│ ├── max@hotel-park-soltau.de/
│ └── anna@hotel-park-soltau.de/
├── _config/
│ └── abteilungen.json # E-Mail → Abteilung Mapping
├── Rezeption/ # Abteilungsvorlagen
├── IT/
├── signatures/
│ ├── headers/ # Persönliche Signatur-Köpfe
│ │ └── max@hotel.de.max-mustermann.html
│ └── footers/ # Gemeinsame Fußbereiche
│ └── Rezeption.html
```
## OR via Thunderbird Add-on Manager
### `_config/abteilungen.json`
Mapping von Abteilungs-E-Mail-Adressen zu Ordnernamen. Wird vom Plugin gelesen, um Abteilung und persönliche E-Mail automatisch zu erkennen:
```json
{
"info@hotel-park-soltau.de": "Rezeption",
"veranstaltungs@hotel-park-soltau.de": "Veranstaltungsbuero",
"it@hotel-park-soltau.de": "IT",
"haustechnik@hotel-park-soltau.de": "Haustechnik"
}
```
Open Thunderbird
Go toTools → Add-ons and Themes
Search for the Templates Reply add-on by name
Click Add to Thunderbird
Confirm installation
## Plugin-Aufbau
| Datei | Funktion |
|---|---|
| `manifest.json` | Extension-Manifest (Thunderbird WebExtension v2) |
| `background.js` | Template-Insertion ins Compose-Fenster |
| `popup.html` / `popup.js` | Popup beim Klick auf "Vorlagen" im Compose |
| `lib/gitea-sync.js` | Gitea-API-Client + Sync-Manager |
| `lib/mdi/` | Material Design Icons (Subset) |
| `templates_options/` | Einstellungsseite (Vorlagen, Signaturen, Verbindung) |
## Installation
### Lokal (Entwicklung)
1. Thunderbird öffnen
2. Extras → Add-ons und Themes
3. Zahnrad-Icon → Add-on aus Datei installieren
4. `templates-reply-hotel.xpi` auswählen
### XPI bauen
```bash
7z a templates-reply-hotel.xpi manifest.json background.js popup.html popup.js lib/ templates_options/ icons/
```
## Einrichtung
1. **Verbindung konfigurieren**: Einstellungen-Tab (⚙) → Server-URL, Repository, Token eingeben → Verbindung speichern
2. **Abteilung wählen** (oder automatisch erkannt via `abteilungen.json`)
3. **Vorlagen erstellen**: Vorlagen-Tab → Neue Vorlage → Sichtbarkeit wählen → Speichern
4. **Signaturen einrichten**: Signaturen-Tab → Identität wählen → Kopfbereich bearbeiten → Speichern
## Voraussetzungen
- Mozilla Thunderbird >= 109.0
- Gitea/Forgejo-Server mit API-Zugang
- API-Token mit Repository-Schreibrechten
## Lizenz
MIT License

View File

@@ -5,6 +5,8 @@ const SYNC_STATE_KEY = 'sync_state';
const TEMPLATE_STORAGE_KEY = 'message_templates';
const SYNC_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
const SHARED_FOLDER = '_gemeinsam';
const USER_FOLDER = '_benutzer';
const CONFIG_FOLDER = '_config';
// ── Gitea API Client ──
@@ -133,6 +135,16 @@ class GiteaClient {
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
return res.json();
}
async getConfig() {
const data = await this.getFile(`${CONFIG_FOLDER}/abteilungen.json`);
if (!data) return null;
try {
return JSON.parse(GiteaClient.fromBase64(data.content));
} catch (_) {
return null;
}
}
}
// ── Sync Manager ──
@@ -162,6 +174,15 @@ class SyncManager {
return this.config?.department || '';
}
get authorEmail() {
return this.config?.authorEmail || '';
}
async autoDetect() {
if (!this.isConfigured) return null;
return await this.client.getConfig();
}
async getSyncState() {
const result = await browser.storage.local.get(SYNC_STATE_KEY);
return result[SYNC_STATE_KEY] || { fileShas: {} };
@@ -200,7 +221,7 @@ class SyncManager {
const entries = await this.client.listDir('');
const departments = [];
for (const entry of entries) {
if (entry.type === 'dir' && entry.name !== SHARED_FOLDER && entry.name !== 'signatures' && !entry.name.startsWith('.')) {
if (entry.type === 'dir' && entry.name !== SHARED_FOLDER && entry.name !== USER_FOLDER && entry.name !== CONFIG_FOLDER && entry.name !== 'signatures' && !entry.name.startsWith('.')) {
departments.push(entry.name);
}
}
@@ -212,10 +233,11 @@ class SyncManager {
*/
async checkRemoteShas() {
if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
if (!this.department) throw new Error('Keine Abteilung ausgewählt');
const remoteShas = {}; // "folder/filename" -> sha
const folders = [SHARED_FOLDER, this.department];
const folders = [SHARED_FOLDER];
if (this.department) folders.push(this.department);
if (this.authorEmail) folders.push(`${USER_FOLDER}/${this.authorEmail}`);
for (const folder of folders) {
const files = await this.client.listDir(folder);
@@ -234,15 +256,16 @@ class SyncManager {
*/
async pullTemplates() {
if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
if (!this.department) throw new Error('Keine Abteilung ausgewählt');
const syncState = await this.getSyncState();
const newTemplates = [];
const newShas = {};
let updated = 0;
// Load from both folders
const folders = [SHARED_FOLDER, this.department];
// Load from shared + department + personal folders
const folders = [SHARED_FOLDER];
if (this.department) folders.push(this.department);
if (this.authorEmail) folders.push(`${USER_FOLDER}/${this.authorEmail}`);
for (const folder of folders) {
const files = await this.client.listDir(folder);
@@ -280,7 +303,6 @@ class SyncManager {
*/
async pushTemplates() {
if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
if (!this.department) throw new Error('Keine Abteilung ausgewählt');
if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)');
const templates = await this.getLocalTemplates();
@@ -289,15 +311,21 @@ class SyncManager {
// Group templates by folder
const byFolder = {};
for (const t of templates) {
const folder = t.folder || this.department;
let folder;
if (t.folder === SHARED_FOLDER) folder = SHARED_FOLDER;
else if (t.folder?.startsWith(USER_FOLDER + '/')) folder = t.folder;
else folder = t.folder || this.department;
if (!folder) continue; // skip if no department and no explicit folder
if (!byFolder[folder]) byFolder[folder] = [];
byFolder[folder].push(t);
}
let pushed = 0;
// Only push to folders the user has templates in
const allowedFolders = [SHARED_FOLDER, this.department];
// Allowed folders: shared + department + personal
const allowedFolders = [SHARED_FOLDER];
if (this.department) allowedFolders.push(this.department);
if (this.authorEmail) allowedFolders.push(`${USER_FOLDER}/${this.authorEmail}`);
for (const folder of allowedFolders) {
const localInFolder = byFolder[folder] || [];
@@ -662,7 +690,7 @@ const syncManager = new SyncManager();
// ── Background message handler for sync ──
browser.runtime.onMessage.addListener(async (msg, sender) => {
const syncActions = ['testConnection', 'pullTemplates', 'pushTemplates', 'pullSingleTemplate', 'pushSingleTemplate', 'deleteRemoteTemplate', 'listDepartments', 'checkRemoteShas', 'pullSignatures', 'pushSignatures', 'loadSignatureTemplate', 'loadFooter', 'pushFooter'];
const syncActions = ['testConnection', 'pullTemplates', 'pushTemplates', 'pullSingleTemplate', 'pushSingleTemplate', 'deleteRemoteTemplate', 'listDepartments', 'checkRemoteShas', 'pullSignatures', 'pushSignatures', 'loadSignatureTemplate', 'loadFooter', 'pushFooter', 'autoDetect'];
if (!syncActions.includes(msg.action)) return;
try {
@@ -713,6 +741,10 @@ browser.runtime.onMessage.addListener(async (msg, sender) => {
case 'pushFooter':
return await syncManager.pushFooter(msg.html);
case 'autoDetect':
const config = await syncManager.autoDetect();
return { success: true, config };
default:
return { success: false, error: 'Unbekannte Aktion' };
}

View File

@@ -27,3 +27,13 @@
.mdi-link::before { content: "\F0337"; }
.mdi-format-clear::before { content: "\F0265"; }
.mdi-image::before { content: "\F02E9"; }
.mdi-cloud-upload::before { content: "\F0167"; }
.mdi-cloud-download::before { content: "\F0162"; }
.mdi-cloud-check::before { content: "\F0157"; }
.mdi-cloud-alert::before { content: "\F09DF"; }
.mdi-sync::before { content: "\F04E6"; }
.mdi-cog::before { content: "\F0493"; }
.mdi-eye::before { content: "\F0208"; }
.mdi-eye-off::before { content: "\F0209"; }
.mdi-account-group::before { content: "\F0849"; }
.mdi-account::before { content: "\F0004"; }

View File

@@ -1,7 +1,7 @@
{
"manifest_version": 2,
"name": "HPS Vorlagen & Signaturen",
"version": "2.0.0",
"version": "2.2.0",
"description": "Vorlagen- und Signaturverwaltung für Hotel Park Soltau mit Git-Sync",
"browser_specific_settings": {
"gecko": {

View File

@@ -96,10 +96,11 @@
<body>
<div class="header">Vorlage auswählen</div>
<div id="prefix-section" class="prefix-section" style="display:none;">
<label for="prefix-select">Voranstellung (optional)</label>
<label for="prefix-select">Textbaustein voranstellen (optional)</label>
<select id="prefix-select">
<option value="">Keine</option>
<option value="">Nichts voranstellen</option>
</select>
<div style="font-size:10px;color:#999;margin-top:2px;">Wird VOR der gewählten Vorlage eingefügt</div>
</div>
<div id="template-list"></div>
<script src="popup.js"></script>

Binary file not shown.

View File

@@ -327,17 +327,25 @@
.template-item:hover { background: #fafff8; }
.template-item input[type="checkbox"] {
width: 16px;
height: 16px;
margin-right: 12px;
width: 18px;
height: 18px;
margin-right: 0;
accent-color: #4a7c59;
cursor: pointer;
flex-shrink: 0;
}
.template-label {
display: flex;
align-items: center;
flex: 1;
cursor: pointer;
gap: 10px;
}
.template-name {
font-weight: 500;
font-size: 13.5px;
flex: 1;
}
.template-actions { display: flex; gap: 6px; }
@@ -354,8 +362,9 @@
.template-actions .push-btn,
.template-actions .pull-btn {
font-weight: bold;
font-size: 14px;
font-weight: 500;
font-size: 12px;
white-space: nowrap;
}
.template-actions .push-btn { color: #4a7c59; }
.template-actions .push-btn:hover { background: #e8f0eb; border-color: #4a7c59; }
@@ -510,6 +519,208 @@
border: 1px dashed #e0e0e0;
border-radius: 6px;
}
/* Badges for shared/private */
.badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
margin-left: 8px;
white-space: nowrap;
}
.badge-shared { background: #e3f2fd; color: #1565c0; }
.badge-dept { background: #fff3e0; color: #e65100; }
.badge-private { background: #f3e5f5; color: #7b1fa2; }
/* Badge as inline select */
select.scope-select {
border: none;
font-size: 11px;
font-weight: 500;
padding: 2px 4px 2px 8px;
border-radius: 10px;
cursor: pointer;
margin-left: 8px;
outline: none;
}
select.scope-select.badge-shared { background: #e3f2fd; color: #1565c0; }
select.scope-select.badge-dept { background: #fff3e0; color: #e65100; }
select.scope-select.badge-private { background: #f3e5f5; color: #7b1fa2; }
/* Toast notifications */
#toast-container {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 9999;
display: flex;
flex-direction: column;
gap: 8px;
max-width: 320px;
}
.toast {
padding: 12px 16px;
border-radius: 8px;
font-size: 13px;
color: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
animation: toast-in 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.toast-success { background: #43a047; }
.toast-error { background: #e53935; }
.toast-info { background: #1e88e5; }
.toast-out { animation: toast-out 0.3s ease forwards; }
@keyframes toast-in { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes toast-out { from { opacity: 1; } to { opacity: 0; transform: translateY(10px); } }
/* Spinner */
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.6s linear infinite;
vertical-align: middle;
}
.spinner-dark {
border-color: rgba(0,0,0,0.15);
border-top-color: #4a7c59;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* Improved sync dots */
.sync-dot.in-sync::after {
content: "✓";
font-size: 8px;
color: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.sync-dot.out-of-sync::after {
content: "!";
font-size: 8px;
font-weight: bold;
color: white;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.sync-dot {
position: relative;
width: 14px;
height: 14px;
}
/* Collapsible improvements */
.collapsible-header {
background: #f9f9f9;
border: 1px solid #e8e8e8;
border-radius: 6px;
padding: 8px 12px;
margin-bottom: 4px;
}
.collapsible-header:hover {
background: #f0f5f1;
border-color: #c0d4c7;
}
.collapsible-header .arrow { font-size: 13px; }
/* Token toggle */
.password-wrapper {
position: relative;
}
.password-wrapper input {
padding-right: 36px;
}
.password-toggle {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
border: none;
background: none;
cursor: pointer;
color: #999;
padding: 2px;
line-height: 1;
}
.password-toggle:hover { color: #555; }
/* Offline banner */
.offline-banner {
display: none;
background: #ff9800;
color: white;
text-align: center;
padding: 10px;
font-size: 13px;
font-weight: 500;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9998;
}
.offline-banner.visible { display: block; }
/* Confirm modal */
.modal-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.4);
z-index: 10000;
align-items: center;
justify-content: center;
}
.modal-backdrop.open { display: flex; }
.modal-card {
background: white;
border-radius: 10px;
padding: 24px;
max-width: 400px;
width: 90%;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.modal-card h3 {
font-size: 16px;
margin-bottom: 12px;
color: #333;
}
.modal-card p {
font-size: 13px;
color: #666;
margin-bottom: 20px;
line-height: 1.5;
}
.modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
/* Validation */
.input-error { border-color: #e53935 !important; }
.input-valid { border-color: #43a047 !important; }
.validation-msg {
font-size: 11px;
margin-top: -6px;
margin-bottom: 8px;
}
.validation-msg.error { color: #e53935; }
.validation-msg.success { color: #43a047; }
</style>
</head>
<body>
@@ -584,9 +795,13 @@
<input type="file" id="tpl-image-file" accept="image/*" style="display:none;">
</div>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
<input type="checkbox" id="tpl-shared-toggle" style="width:16px;height:16px;accent-color:#4a7c59;">
<label for="tpl-shared-toggle" style="margin-bottom:0;cursor:pointer;">Für alle Abteilungen (<code>_gemeinsam</code>)</label>
<div class="form-group">
<label for="tpl-scope-select">Sichtbarkeit</label>
<select id="tpl-scope-select" style="width:100%;">
<option value="private" selected>Persönlich (nur ich, auf allen meinen Geräten)</option>
<option value="department">Meine Abteilung</option>
<option value="shared">Alle Abteilungen</option>
</select>
</div>
<button type="submit" class="btn btn-primary" id="save-button">Speichern</button>
@@ -811,7 +1026,10 @@
</div>
<div class="form-group">
<label for="sync-token">API-Token</label>
<input type="password" id="sync-token" placeholder="Token aus Gitea-Einstellungen">
<div class="password-wrapper">
<input type="password" id="sync-token" placeholder="Token aus Gitea-Einstellungen">
<button type="button" class="password-toggle" id="token-toggle" title="Token anzeigen/verbergen"><span class="mdi mdi-eye"></span></button>
</div>
</div>
</div>
@@ -825,6 +1043,24 @@
<div id="sync-log"></div>
</div>
<!-- Offline Banner -->
<div id="offline-banner" class="offline-banner">Keine Internetverbindung Synchronisation nicht möglich</div>
<!-- Toast Container -->
<div id="toast-container"></div>
<!-- Confirm Modal -->
<div id="confirm-modal" class="modal-backdrop">
<div class="modal-card">
<h3 id="modal-title"></h3>
<p id="modal-message"></p>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" id="modal-cancel">Abbrechen</button>
<button type="button" class="btn btn-danger" id="modal-confirm">Löschen</button>
</div>
</div>
</div>
<script src="templates_options.js"></script>
</body>
</html>

View File

@@ -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">&#8593;</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">&#8595;</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 ──