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:
@@ -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' };
|
||||
}
|
||||
|
||||
@@ -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"; }
|
||||
|
||||
Reference in New Issue
Block a user