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

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' };
}