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:
130
README.md
130
README.md
@@ -1,38 +1,92 @@
|
|||||||
# Templates Reply
|
# HPS Vorlagen & Signaturen
|
||||||
## Mozilla Thunderbird Add-On
|
|
||||||
|
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.
|
||||||
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.
|
## Features
|
||||||
|
|
||||||
This add-on does not track, store, or transmit any personal information.
|
- **E-Mail-Vorlagen** erstellen, bearbeiten und per Klick in Compose-Fenster einfügen
|
||||||
All templates and settings are saved locally on your device and never leave your computer.
|
- **3 Sichtbarkeitsstufen** pro Vorlage:
|
||||||
No analytics, telemetry, or remote servers are involved — your privacy is 100% respected.
|
- **Persönlich** — nur für den eigenen Account, gesynct in `_benutzer/{email}/`
|
||||||
|
- **Abteilung** — für alle in der Abteilung, gesynct in den Abteilungsordner
|
||||||
You can freely use, modify, and distribute the source code under the terms of the MIT License.
|
- **Alle Abteilungen** — firmenweit, gesynct in `_gemeinsam/`
|
||||||
## Screenshots
|
- **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
|
||||||
|
|
||||||
|
## Repository-Struktur (Gitea)
|
||||||
## Install Locally (Temporary / Developer Mode)
|
|
||||||
```
|
```
|
||||||
Open Thunderbird
|
repo/
|
||||||
Go to Tools → Add-ons and Themes
|
├── _gemeinsam/ # Vorlagen für alle Abteilungen
|
||||||
Click the ⚙️ (gear icon) in the top-right corner
|
│ └── beispiel-vorlage.html
|
||||||
Select “Debug Add-ons”
|
├── _benutzer/ # Persönliche Vorlagen pro User
|
||||||
Click “Load Temporary Add-on…”
|
│ ├── max@hotel-park-soltau.de/
|
||||||
Choose the manifest.json file from your add-on directory
|
│ └── anna@hotel-park-soltau.de/
|
||||||
|
├── _config/
|
||||||
⚠️ Note: This installation is temporary and will be removed when Thunderbird is restarted.
|
│ └── abteilungen.json # E-Mail → Abteilung Mapping
|
||||||
```
|
├── Rezeption/ # Abteilungsvorlagen
|
||||||
|
├── IT/
|
||||||
## OR via Thunderbird Add-on Manager
|
├── signatures/
|
||||||
```
|
│ ├── headers/ # Persönliche Signatur-Köpfe
|
||||||
Open Thunderbird
|
│ │ └── max@hotel.de.max-mustermann.html
|
||||||
Go toTools → Add-ons and Themes
|
│ └── footers/ # Gemeinsame Fußbereiche
|
||||||
Search for the Templates Reply add-on by name
|
│ └── Rezeption.html
|
||||||
Click Add to Thunderbird
|
```
|
||||||
Confirm installation
|
|
||||||
```
|
### `_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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ const SYNC_STATE_KEY = 'sync_state';
|
|||||||
const TEMPLATE_STORAGE_KEY = 'message_templates';
|
const TEMPLATE_STORAGE_KEY = 'message_templates';
|
||||||
const SYNC_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
const SYNC_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
||||||
const SHARED_FOLDER = '_gemeinsam';
|
const SHARED_FOLDER = '_gemeinsam';
|
||||||
|
const USER_FOLDER = '_benutzer';
|
||||||
|
const CONFIG_FOLDER = '_config';
|
||||||
|
|
||||||
// ── Gitea API Client ──
|
// ── Gitea API Client ──
|
||||||
|
|
||||||
@@ -133,6 +135,16 @@ class GiteaClient {
|
|||||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
||||||
return res.json();
|
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 ──
|
// ── Sync Manager ──
|
||||||
@@ -162,6 +174,15 @@ class SyncManager {
|
|||||||
return this.config?.department || '';
|
return this.config?.department || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get authorEmail() {
|
||||||
|
return this.config?.authorEmail || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async autoDetect() {
|
||||||
|
if (!this.isConfigured) return null;
|
||||||
|
return await this.client.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
async getSyncState() {
|
async getSyncState() {
|
||||||
const result = await browser.storage.local.get(SYNC_STATE_KEY);
|
const result = await browser.storage.local.get(SYNC_STATE_KEY);
|
||||||
return result[SYNC_STATE_KEY] || { fileShas: {} };
|
return result[SYNC_STATE_KEY] || { fileShas: {} };
|
||||||
@@ -200,7 +221,7 @@ class SyncManager {
|
|||||||
const entries = await this.client.listDir('');
|
const entries = await this.client.listDir('');
|
||||||
const departments = [];
|
const departments = [];
|
||||||
for (const entry of entries) {
|
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);
|
departments.push(entry.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,10 +233,11 @@ class SyncManager {
|
|||||||
*/
|
*/
|
||||||
async checkRemoteShas() {
|
async checkRemoteShas() {
|
||||||
if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
|
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 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) {
|
for (const folder of folders) {
|
||||||
const files = await this.client.listDir(folder);
|
const files = await this.client.listDir(folder);
|
||||||
@@ -234,15 +256,16 @@ class SyncManager {
|
|||||||
*/
|
*/
|
||||||
async pullTemplates() {
|
async pullTemplates() {
|
||||||
if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
|
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 syncState = await this.getSyncState();
|
||||||
const newTemplates = [];
|
const newTemplates = [];
|
||||||
const newShas = {};
|
const newShas = {};
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
|
|
||||||
// Load from both folders
|
// Load from shared + department + personal folders
|
||||||
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) {
|
for (const folder of folders) {
|
||||||
const files = await this.client.listDir(folder);
|
const files = await this.client.listDir(folder);
|
||||||
@@ -280,7 +303,6 @@ class SyncManager {
|
|||||||
*/
|
*/
|
||||||
async pushTemplates() {
|
async pushTemplates() {
|
||||||
if (!this.isConfigured) throw new Error('Sync nicht konfiguriert');
|
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)');
|
if (!this.config.authorName) throw new Error('Bitte Name eintragen (für Commit-Zuordnung)');
|
||||||
|
|
||||||
const templates = await this.getLocalTemplates();
|
const templates = await this.getLocalTemplates();
|
||||||
@@ -289,15 +311,21 @@ class SyncManager {
|
|||||||
// Group templates by folder
|
// Group templates by folder
|
||||||
const byFolder = {};
|
const byFolder = {};
|
||||||
for (const t of templates) {
|
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] = [];
|
if (!byFolder[folder]) byFolder[folder] = [];
|
||||||
byFolder[folder].push(t);
|
byFolder[folder].push(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pushed = 0;
|
let pushed = 0;
|
||||||
|
|
||||||
// Only push to folders the user has templates in
|
// Allowed folders: shared + department + personal
|
||||||
const allowedFolders = [SHARED_FOLDER, this.department];
|
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) {
|
for (const folder of allowedFolders) {
|
||||||
const localInFolder = byFolder[folder] || [];
|
const localInFolder = byFolder[folder] || [];
|
||||||
@@ -662,7 +690,7 @@ const syncManager = new SyncManager();
|
|||||||
// ── Background message handler for sync ──
|
// ── Background message handler for sync ──
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener(async (msg, sender) => {
|
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;
|
if (!syncActions.includes(msg.action)) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -713,6 +741,10 @@ browser.runtime.onMessage.addListener(async (msg, sender) => {
|
|||||||
case 'pushFooter':
|
case 'pushFooter':
|
||||||
return await syncManager.pushFooter(msg.html);
|
return await syncManager.pushFooter(msg.html);
|
||||||
|
|
||||||
|
case 'autoDetect':
|
||||||
|
const config = await syncManager.autoDetect();
|
||||||
|
return { success: true, config };
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return { success: false, error: 'Unbekannte Aktion' };
|
return { success: false, error: 'Unbekannte Aktion' };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,3 +27,13 @@
|
|||||||
.mdi-link::before { content: "\F0337"; }
|
.mdi-link::before { content: "\F0337"; }
|
||||||
.mdi-format-clear::before { content: "\F0265"; }
|
.mdi-format-clear::before { content: "\F0265"; }
|
||||||
.mdi-image::before { content: "\F02E9"; }
|
.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"; }
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "HPS Vorlagen & Signaturen",
|
"name": "HPS Vorlagen & Signaturen",
|
||||||
"version": "2.0.0",
|
"version": "2.2.0",
|
||||||
"description": "Vorlagen- und Signaturverwaltung für Hotel Park Soltau mit Git-Sync",
|
"description": "Vorlagen- und Signaturverwaltung für Hotel Park Soltau mit Git-Sync",
|
||||||
"browser_specific_settings": {
|
"browser_specific_settings": {
|
||||||
"gecko": {
|
"gecko": {
|
||||||
|
|||||||
@@ -96,10 +96,11 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="header">Vorlage auswählen</div>
|
<div class="header">Vorlage auswählen</div>
|
||||||
<div id="prefix-section" class="prefix-section" style="display:none;">
|
<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">
|
<select id="prefix-select">
|
||||||
<option value="">— Keine —</option>
|
<option value="">— Nichts voranstellen —</option>
|
||||||
</select>
|
</select>
|
||||||
|
<div style="font-size:10px;color:#999;margin-top:2px;">Wird VOR der gewählten Vorlage eingefügt</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="template-list"></div>
|
<div id="template-list"></div>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
|
|||||||
Binary file not shown.
@@ -327,17 +327,25 @@
|
|||||||
.template-item:hover { background: #fafff8; }
|
.template-item:hover { background: #fafff8; }
|
||||||
|
|
||||||
.template-item input[type="checkbox"] {
|
.template-item input[type="checkbox"] {
|
||||||
width: 16px;
|
width: 18px;
|
||||||
height: 16px;
|
height: 18px;
|
||||||
margin-right: 12px;
|
margin-right: 0;
|
||||||
accent-color: #4a7c59;
|
accent-color: #4a7c59;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-name {
|
.template-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 13.5px;
|
font-size: 13.5px;
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-actions { display: flex; gap: 6px; }
|
.template-actions { display: flex; gap: 6px; }
|
||||||
@@ -354,8 +362,9 @@
|
|||||||
|
|
||||||
.template-actions .push-btn,
|
.template-actions .push-btn,
|
||||||
.template-actions .pull-btn {
|
.template-actions .pull-btn {
|
||||||
font-weight: bold;
|
font-weight: 500;
|
||||||
font-size: 14px;
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.template-actions .push-btn { color: #4a7c59; }
|
.template-actions .push-btn { color: #4a7c59; }
|
||||||
.template-actions .push-btn:hover { background: #e8f0eb; border-color: #4a7c59; }
|
.template-actions .push-btn:hover { background: #e8f0eb; border-color: #4a7c59; }
|
||||||
@@ -510,6 +519,208 @@
|
|||||||
border: 1px dashed #e0e0e0;
|
border: 1px dashed #e0e0e0;
|
||||||
border-radius: 6px;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -584,9 +795,13 @@
|
|||||||
<input type="file" id="tpl-image-file" accept="image/*" style="display:none;">
|
<input type="file" id="tpl-image-file" accept="image/*" style="display:none;">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px;">
|
<div class="form-group">
|
||||||
<input type="checkbox" id="tpl-shared-toggle" style="width:16px;height:16px;accent-color:#4a7c59;">
|
<label for="tpl-scope-select">Sichtbarkeit</label>
|
||||||
<label for="tpl-shared-toggle" style="margin-bottom:0;cursor:pointer;">Für alle Abteilungen (<code>_gemeinsam</code>)</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>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary" id="save-button">Speichern</button>
|
<button type="submit" class="btn btn-primary" id="save-button">Speichern</button>
|
||||||
@@ -811,7 +1026,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sync-token">API-Token</label>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -825,6 +1043,24 @@
|
|||||||
<div id="sync-log"></div>
|
<div id="sync-log"></div>
|
||||||
</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>
|
<script src="templates_options.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -6,6 +6,112 @@ const SYNC_CONFIG_KEY = 'gitea_config';
|
|||||||
const SIG_SOURCE_KEY = 'sig_source_map';
|
const SIG_SOURCE_KEY = 'sig_source_map';
|
||||||
const SIG_FOOTER_KEY = 'sig_footer_cache';
|
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 ──
|
// ── DOM Elements ──
|
||||||
|
|
||||||
const editorPanel = document.getElementById('tpl-editor-panel');
|
const editorPanel = document.getElementById('tpl-editor-panel');
|
||||||
@@ -62,7 +168,20 @@ function renderFontDropdown(filter) {
|
|||||||
});
|
});
|
||||||
fontDropdown.appendChild(div);
|
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));
|
fontInput.addEventListener('focus', () => renderFontDropdown(fontInput.value));
|
||||||
@@ -158,7 +277,20 @@ function renderSigFontDropdown(filter) {
|
|||||||
});
|
});
|
||||||
sigFontDropdown.appendChild(div);
|
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));
|
sigFontInput.addEventListener('focus', () => renderSigFontDropdown(sigFontInput.value));
|
||||||
@@ -374,19 +506,24 @@ function renderTemplates(templates) {
|
|||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'template-item';
|
item.className = 'template-item';
|
||||||
const syncClass = getTplSyncClass(template);
|
const syncClass = getTplSyncClass(template);
|
||||||
const folderBadge = template.folder
|
const scopeValue = template.folder === '_gemeinsam' ? 'shared'
|
||||||
? `<span style="font-size:11px;color:#999;margin-left:6px;">[${template.folder}]</span>`
|
: 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'
|
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)
|
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 = `
|
item.innerHTML = `
|
||||||
<span class="sync-dot ${syncClass}" title="${syncClass === 'in-sync' ? 'Synchron' : syncClass === 'out-of-sync' ? 'Nicht hochgeladen' : 'Unbekannt'}"></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>
|
||||||
<input type="checkbox" class="template-checkbox" data-id="${template.id}">
|
<label class="template-label"><input type="checkbox" class="template-checkbox" data-id="${template.id}"><span class="template-name">${template.name}</span>${folderBadge}</label>
|
||||||
<span class="template-name">${template.name}${folderBadge}</span>
|
|
||||||
<div class="template-actions">
|
<div class="template-actions">
|
||||||
${pullBtn}${pushBtn}
|
${pullBtn}${pushBtn}
|
||||||
<button data-id="${template.id}" class="edit-btn">Bearbeiten</button>
|
<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('.delete-btn').forEach(b => b.addEventListener('click', handleDelete));
|
||||||
document.querySelectorAll('.push-btn').forEach(b => b.addEventListener('click', handlePushSingle));
|
document.querySelectorAll('.push-btn').forEach(b => b.addEventListener('click', handlePushSingle));
|
||||||
document.querySelectorAll('.pull-btn').forEach(b => b.addEventListener('click', handlePullSingle));
|
document.querySelectorAll('.pull-btn').forEach(b => b.addEventListener('click', handlePullSingle));
|
||||||
|
document.querySelectorAll('.scope-select').forEach(sel => sel.addEventListener('change', handleScopeChange));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Inline Editor Panel ──
|
// ── Inline Editor Panel ──
|
||||||
|
|
||||||
function openEditorPanel() {
|
function openEditorPanel() {
|
||||||
editorPanel.classList.add('open');
|
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' });
|
editorPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,7 +562,7 @@ function closeEditorPanel() {
|
|||||||
|
|
||||||
document.getElementById('new-template-button').addEventListener('click', () => {
|
document.getElementById('new-template-button').addEventListener('click', () => {
|
||||||
closeEditorPanel();
|
closeEditorPanel();
|
||||||
document.getElementById('tpl-shared-toggle').checked = false;
|
document.getElementById('tpl-scope-select').value = 'private';
|
||||||
formLegend.textContent = 'Neue Vorlage erstellen';
|
formLegend.textContent = 'Neue Vorlage erstellen';
|
||||||
saveButton.textContent = 'Speichern';
|
saveButton.textContent = 'Speichern';
|
||||||
openEditorPanel();
|
openEditorPanel();
|
||||||
@@ -437,30 +579,73 @@ templateForm.addEventListener('submit', async (e) => {
|
|||||||
const content = getEditorContent();
|
const content = getEditorContent();
|
||||||
|
|
||||||
if (!content.trim()) {
|
if (!content.trim()) {
|
||||||
alert('Bitte Inhalt eingeben.');
|
showToast('Bitte Inhalt eingeben.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let templates = await getTemplates();
|
let templates = await getTemplates();
|
||||||
|
|
||||||
const isShared = document.getElementById('tpl-shared-toggle').checked;
|
const scope = document.getElementById('tpl-scope-select').value;
|
||||||
const folder = isShared ? '_gemeinsam' : undefined;
|
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) {
|
if (id) {
|
||||||
const index = templates.findIndex(t => t.id === id);
|
const index = templates.findIndex(t => t.id === id);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
const remotePath = templates[index].remotePath;
|
const existingTemplate = templates[index];
|
||||||
const effectiveFolder = folder !== undefined ? folder : templates[index].folder;
|
const oldFolder = existingTemplate.folder;
|
||||||
templates[index] = { id, name, content, folder: effectiveFolder, remotePath };
|
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 {
|
} else {
|
||||||
templates.push({ id: Date.now().toString(), name, content, folder: folder || undefined });
|
templates.push({ id: Date.now().toString(), name, content, folder });
|
||||||
}
|
}
|
||||||
|
|
||||||
await saveTemplates(templates);
|
await saveTemplates(templates);
|
||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
closeEditorPanel();
|
closeEditorPanel();
|
||||||
updateTplSyncIndicator();
|
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 ──
|
// ── Edit / Delete / Push / Pull ──
|
||||||
@@ -473,7 +658,10 @@ async function handleEdit(e) {
|
|||||||
|
|
||||||
document.getElementById('template-id').value = template.id;
|
document.getElementById('template-id').value = template.id;
|
||||||
document.getElementById('template-name').value = template.name;
|
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);
|
setEditorContent(template.content);
|
||||||
formLegend.textContent = 'Vorlage bearbeiten';
|
formLegend.textContent = 'Vorlage bearbeiten';
|
||||||
saveButton.textContent = 'Aktualisieren';
|
saveButton.textContent = 'Aktualisieren';
|
||||||
@@ -487,12 +675,12 @@ async function handleDelete(e) {
|
|||||||
if (!template) return;
|
if (!template) return;
|
||||||
|
|
||||||
if (template.remotePath) {
|
if (template.remotePath) {
|
||||||
const choice = confirm(
|
const confirmed = await showConfirmDialog(
|
||||||
`"${template.name}" löschen?\n\n` +
|
`"${template.name}" löschen?`,
|
||||||
`OK = Lokal UND vom Server löschen (für alle)\n` +
|
'Die Vorlage wird lokal und vom Server gelöscht (für alle Nutzer).',
|
||||||
`Abbrechen = Nicht löschen`
|
'Überall löschen'
|
||||||
);
|
);
|
||||||
if (!choice) return;
|
if (!confirmed) return;
|
||||||
|
|
||||||
// Delete from server
|
// Delete from server
|
||||||
try {
|
try {
|
||||||
@@ -501,29 +689,37 @@ async function handleDelete(e) {
|
|||||||
remotePath: template.remotePath
|
remotePath: template.remotePath
|
||||||
});
|
});
|
||||||
if (!result?.success) {
|
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) {
|
} catch (err) {
|
||||||
alert('Fehler beim Löschen vom Server: ' + err.message);
|
showToast('Fehler beim Löschen vom Server: ' + err.message, 'error', 6000);
|
||||||
}
|
}
|
||||||
} else {
|
} 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);
|
templates = templates.filter(t => t.id !== id);
|
||||||
await saveTemplates(templates);
|
await saveTemplates(templates);
|
||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
updateTplSyncIndicator();
|
updateTplSyncIndicator();
|
||||||
|
showToast('Vorlage gelöscht.', 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePullSingle(e) {
|
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 templates = await getTemplates();
|
||||||
const template = templates.find(t => t.id === id);
|
const template = templates.find(t => t.id === id);
|
||||||
if (!template || !template.remotePath) return;
|
if (!template || !template.remotePath) return;
|
||||||
|
if (!checkOnline()) return;
|
||||||
|
|
||||||
e.target.textContent = '...';
|
const origHTML = btn.innerHTML;
|
||||||
e.target.disabled = true;
|
btn.innerHTML = '<span class="spinner"></span>';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -540,26 +736,30 @@ async function handlePullSingle(e) {
|
|||||||
saveSyncHashes();
|
saveSyncHashes();
|
||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
updateTplSyncIndicator();
|
updateTplSyncIndicator();
|
||||||
|
showToast('Vorlage heruntergeladen.', 'success');
|
||||||
} else {
|
} else {
|
||||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||||||
e.target.textContent = '\u2193';
|
btn.innerHTML = origHTML;
|
||||||
e.target.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Fehler: ' + err.message);
|
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||||
e.target.textContent = '\u2193';
|
btn.innerHTML = origHTML;
|
||||||
e.target.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handlePushSingle(e) {
|
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 templates = await getTemplates();
|
||||||
const template = templates.find(t => t.id === id);
|
const template = templates.find(t => t.id === id);
|
||||||
if (!template) return;
|
if (!template) return;
|
||||||
|
if (!checkOnline()) return;
|
||||||
|
|
||||||
e.target.textContent = '...';
|
const origHTML = btn.innerHTML;
|
||||||
e.target.disabled = true;
|
btn.innerHTML = '<span class="spinner"></span>';
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({
|
const result = await browser.runtime.sendMessage({
|
||||||
@@ -571,18 +771,87 @@ async function handlePushSingle(e) {
|
|||||||
saveSyncHashes();
|
saveSyncHashes();
|
||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
updateTplSyncIndicator();
|
updateTplSyncIndicator();
|
||||||
|
showToast('Vorlage hochgeladen.', 'success');
|
||||||
} else {
|
} else {
|
||||||
alert('Fehler: ' + (result?.error || 'Unbekannt'));
|
showToast('Fehler: ' + (result?.error || 'Unbekannt'), 'error', 6000);
|
||||||
e.target.textContent = '\u2191';
|
btn.innerHTML = origHTML;
|
||||||
e.target.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Fehler: ' + err.message);
|
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||||
e.target.textContent = '\u2191';
|
btn.innerHTML = origHTML;
|
||||||
e.target.disabled = false;
|
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 ──
|
// ── Bulk Actions ──
|
||||||
|
|
||||||
document.getElementById('select-all-button').addEventListener('click', () => {
|
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 () => {
|
document.getElementById('delete-selected-button').addEventListener('click', async () => {
|
||||||
const checked = document.querySelectorAll('.template-checkbox:checked');
|
const checked = document.querySelectorAll('.template-checkbox:checked');
|
||||||
if (checked.length === 0) return;
|
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));
|
const idsToDelete = new Set(Array.from(checked).map(cb => cb.dataset.id));
|
||||||
let templates = await getTemplates();
|
let templates = await getTemplates();
|
||||||
templates = templates.filter(t => !idsToDelete.has(t.id));
|
templates = templates.filter(t => !idsToDelete.has(t.id));
|
||||||
await saveTemplates(templates);
|
await saveTemplates(templates);
|
||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
|
showToast(`${checked.length} Vorlage(n) gelöscht.`, 'info');
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── HTML File Import ──
|
// ── HTML File Import ──
|
||||||
@@ -611,9 +886,7 @@ document.getElementById('import-button').addEventListener('click', async () => {
|
|||||||
const files = fileInput.files;
|
const files = fileInput.files;
|
||||||
|
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
statusEl.textContent = 'Bitte Dateien auswählen!';
|
showToast('Bitte Dateien auswählen!', 'error');
|
||||||
statusEl.style.color = 'red';
|
|
||||||
statusEl.style.display = 'inline';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,28 +913,26 @@ document.getElementById('import-button').addEventListener('click', async () => {
|
|||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
updateTplSyncIndicator();
|
updateTplSyncIndicator();
|
||||||
|
|
||||||
statusEl.textContent = `${importCount} Vorlage(n) importiert!`;
|
showToast(`${importCount} Vorlage(n) importiert!`, 'success');
|
||||||
statusEl.style.color = 'green';
|
|
||||||
statusEl.style.display = 'inline';
|
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
setTimeout(() => { statusEl.style.display = 'none'; }, 3000);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Sync "Aktualisieren" Button (Pull + Push) ──
|
// ── Sync "Aktualisieren" Button (Pull + Push) ──
|
||||||
|
|
||||||
document.getElementById('sync-refresh-button').addEventListener('click', async () => {
|
document.getElementById('sync-refresh-button').addEventListener('click', async () => {
|
||||||
const statusEl = document.getElementById('sync-sync-status');
|
if (!checkOnline()) return;
|
||||||
statusEl.textContent = 'Synchronisiere...';
|
const btn = document.getElementById('sync-refresh-button');
|
||||||
statusEl.style.color = '#777';
|
const origText = btn.textContent;
|
||||||
statusEl.style.display = 'inline';
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner spinner-dark"></span> Synchronisiere...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Pull first
|
// Pull first
|
||||||
const pullResult = await browser.runtime.sendMessage({ action: 'pullTemplates' });
|
const pullResult = await browser.runtime.sendMessage({ action: 'pullTemplates' });
|
||||||
if (!pullResult?.success) {
|
if (!pullResult?.success) {
|
||||||
statusEl.textContent = pullResult?.error || 'Fehler beim Laden';
|
showToast(pullResult?.error || 'Fehler beim Laden', 'error', 6000);
|
||||||
statusEl.style.color = 'red';
|
btn.disabled = false;
|
||||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
btn.textContent = origText;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,14 +945,12 @@ document.getElementById('sync-refresh-button').addEventListener('click', async (
|
|||||||
renderTemplates(templates);
|
renderTemplates(templates);
|
||||||
updateTplSyncIndicator();
|
updateTplSyncIndicator();
|
||||||
|
|
||||||
const msg = `${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`;
|
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
|
||||||
statusEl.textContent = msg;
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = 'Fehler: ' + err.message;
|
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||||
statusEl.style.color = 'red';
|
|
||||||
}
|
}
|
||||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
btn.disabled = false;
|
||||||
|
btn.textContent = origText;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Signaturen ──
|
// ── Signaturen ──
|
||||||
@@ -756,6 +1025,12 @@ async function loadIdentities() {
|
|||||||
sigIdentitySelect.appendChild(opt);
|
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
|
// 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) {
|
function showSigStatus(message, color) {
|
||||||
const el = document.getElementById('sig-status');
|
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||||
el.textContent = message;
|
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||||
el.style.color = color;
|
|
||||||
el.style.display = 'inline';
|
|
||||||
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save signature to Thunderbird identity (header + footer)
|
// 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)
|
// Signature sync - "Aktualisieren" (pull + push)
|
||||||
document.getElementById('sig-sync-refresh').addEventListener('click', async () => {
|
document.getElementById('sig-sync-refresh').addEventListener('click', async () => {
|
||||||
const statusEl = document.getElementById('sig-sync-status');
|
if (!checkOnline()) return;
|
||||||
statusEl.textContent = 'Synchronisiere...';
|
const btn = document.getElementById('sig-sync-refresh');
|
||||||
statusEl.style.color = '#777';
|
const origText = btn.textContent;
|
||||||
statusEl.style.display = 'inline';
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<span class="spinner spinner-dark"></span> Synchronisiere...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Pull first (gets footer + headers)
|
// Pull first (gets footer + headers)
|
||||||
const pullResult = await browser.runtime.sendMessage({ action: 'pullSignatures' });
|
const pullResult = await browser.runtime.sendMessage({ action: 'pullSignatures' });
|
||||||
if (!pullResult?.success) {
|
if (!pullResult?.success) {
|
||||||
statusEl.textContent = pullResult?.error || 'Fehler';
|
showToast(pullResult?.error || 'Fehler', 'error', 6000);
|
||||||
statusEl.style.color = 'red';
|
btn.disabled = false;
|
||||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
btn.textContent = origText;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,14 +1278,12 @@ document.getElementById('sig-sync-refresh').addEventListener('click', async () =
|
|||||||
updateSigSyncIndicator();
|
updateSigSyncIndicator();
|
||||||
sigIdentitySelect.dispatchEvent(new Event('change'));
|
sigIdentitySelect.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
const msg = `${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`;
|
showToast(`${pullResult.updated || 0} geladen, ${pushResult?.pushed || 0} hochgeladen`, 'success');
|
||||||
statusEl.textContent = msg;
|
|
||||||
statusEl.style.color = 'green';
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statusEl.textContent = 'Fehler: ' + err.message;
|
showToast('Fehler: ' + err.message, 'error', 6000);
|
||||||
statusEl.style.color = 'red';
|
|
||||||
}
|
}
|
||||||
setTimeout(() => { statusEl.style.display = 'none'; }, 4000);
|
btn.disabled = false;
|
||||||
|
btn.textContent = origText;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Footer Editor ──
|
// ── Footer Editor ──
|
||||||
@@ -1038,11 +1309,8 @@ document.getElementById('footer-toggle').addEventListener('click', async functio
|
|||||||
});
|
});
|
||||||
|
|
||||||
function showFooterStatus(message, color) {
|
function showFooterStatus(message, color) {
|
||||||
const el = document.getElementById('footer-status');
|
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||||
el.textContent = message;
|
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||||
el.style.color = color;
|
|
||||||
el.style.display = 'inline';
|
|
||||||
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load footer from server
|
// Load footer from server
|
||||||
@@ -1096,6 +1364,7 @@ async function loadSyncConfig() {
|
|||||||
document.getElementById('sync-token').value = config.token || '';
|
document.getElementById('sync-token').value = config.token || '';
|
||||||
document.getElementById('sync-author-name').value = config.authorName || '';
|
document.getElementById('sync-author-name').value = config.authorName || '';
|
||||||
document.getElementById('sync-author-email').value = config.authorEmail || '';
|
document.getElementById('sync-author-email').value = config.authorEmail || '';
|
||||||
|
cachedAuthorEmail = config.authorEmail || '';
|
||||||
if (config.baseUrl && config.token) {
|
if (config.baseUrl && config.token) {
|
||||||
updateSyncStatus('connected', 'Verbunden');
|
updateSyncStatus('connected', 'Verbunden');
|
||||||
}
|
}
|
||||||
@@ -1108,10 +1377,105 @@ async function loadSyncConfig() {
|
|||||||
deptSelect.appendChild(opt);
|
deptSelect.appendChild(opt);
|
||||||
}
|
}
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
|
|
||||||
|
// Auto-detect department + author from server config
|
||||||
|
if (config.baseUrl && config.token) {
|
||||||
|
tryAutoDetect(config);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} 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() {
|
function getSyncConfigFromForm() {
|
||||||
return {
|
return {
|
||||||
baseUrl: document.getElementById('sync-url').value.replace(/\/+$/, ''),
|
baseUrl: document.getElementById('sync-url').value.replace(/\/+$/, ''),
|
||||||
@@ -1132,11 +1496,8 @@ function updateSyncStatus(type, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showSyncActionStatus(elId, message, color) {
|
function showSyncActionStatus(elId, message, color) {
|
||||||
const el = document.getElementById(elId);
|
const type = color === 'green' ? 'success' : color === 'red' ? 'error' : 'info';
|
||||||
el.textContent = message;
|
showToast(message, type, type === 'error' ? 6000 : 4000);
|
||||||
el.style.color = color;
|
|
||||||
el.style.display = 'inline';
|
|
||||||
setTimeout(() => { el.style.display = 'none'; }, 4000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendSyncLog(message) {
|
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('refresh-departments').addEventListener('click', loadDepartments);
|
||||||
|
|
||||||
document.getElementById('test-sync-connection').addEventListener('click', async () => {
|
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 {
|
try {
|
||||||
const result = await browser.runtime.sendMessage({ action: 'testConnection' });
|
const result = await browser.runtime.sendMessage({ action: 'testConnection' });
|
||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
updateSyncStatus('connected', 'Verbunden');
|
updateSyncStatus('connected', 'Verbunden');
|
||||||
showSyncActionStatus('sync-action-status', 'Verbindung erfolgreich!', 'green');
|
showToast('Verbindung erfolgreich!', 'success');
|
||||||
appendSyncLog('Verbindungstest erfolgreich.');
|
appendSyncLog('Verbindungstest erfolgreich.');
|
||||||
loadDepartments();
|
loadDepartments();
|
||||||
} else {
|
} else {
|
||||||
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
||||||
showSyncActionStatus('sync-action-status', result?.error || 'Fehler', 'red');
|
showToast(result?.error || 'Fehler', 'error', 6000);
|
||||||
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
|
appendSyncLog('Verbindungstest fehlgeschlagen: ' + (result?.error || 'Unbekannt'));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
updateSyncStatus('error', 'Verbindung fehlgeschlagen');
|
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 ──
|
// ── Init ──
|
||||||
|
|||||||
Reference in New Issue
Block a user