diff --git a/background.js b/background.js index 8465da5..0274d07 100644 --- a/background.js +++ b/background.js @@ -1,5 +1,109 @@ +// ── Toolbar button: open settings page ── + +browser.browserAction.onClicked.addListener(() => { + browser.runtime.openOptionsPage(); +}); + +// ── "Erledigt" button in message display ── + +async function executeErledigtAction(tab, actionConfig) { + const message = await messenger.messageDisplay.getDisplayedMessage(tab.id); + if (!message) { + browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler', message: 'Keine Nachricht ausgewählt.' }); + return; + } + + const storage = await browser.storage.local.get(['gitea_config', 'schlagwoerter_cache']); + const config = storage.gitea_config || {}; + const schlagwoerter = storage.schlagwoerter_cache; + + // Apply user's tag + let tagKey = null; + if (Array.isArray(schlagwoerter) && config.authorName) { + const match = schlagwoerter.find(u => u.name.toLowerCase() === config.authorName.toLowerCase()); + if (match) { + tagKey = `$hps_${match.name.toLowerCase().replace(/\s+/g, '_')}`; + } + } + + if (tagKey) { + const currentTags = message.tags || []; + if (!currentTags.includes(tagKey)) { + await messenger.messages.update(message.id, { tags: [...currentTags, tagKey] }); + } + } + + // Move to target folder + if (actionConfig.targetFolder) { + const folderInfo = JSON.parse(actionConfig.targetFolder); + await messenger.messages.move([message.id], folderInfo); + } + + // Feedback + const parts = []; + if (tagKey) parts.push('markiert'); + if (actionConfig.targetFolder) parts.push('verschoben'); + const title = actionConfig.name || 'Erledigt'; + browser.notifications.create({ + type: 'basic', + iconUrl: browser.runtime.getURL('icons/icon.png'), + title, + message: parts.length ? `Nachricht ${parts.join(' & ')}.` : 'Kein Schlagwort oder Zielordner konfiguriert.' + }); +} + +// Single action: direct click without popup +messenger.messageDisplayAction.onClicked.addListener(async (tab) => { + try { + const result = await browser.storage.local.get('erledigt_config'); + const actions = (result.erledigt_config || {}).actions || []; + await executeErledigtAction(tab, actions[0] || {}); + } catch (e) { + console.error('Erledigt-Button Fehler:', e); + browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler', message: e.message }); + } +}); + +// Toggle popup vs direct click based on action count +async function updateErledigtPopup() { + const result = await browser.storage.local.get('erledigt_config'); + const actions = (result.erledigt_config || {}).actions || []; + if (actions.length > 1) { + await messenger.messageDisplayAction.setPopup({ popup: 'message_popup.html' }); + await messenger.messageDisplayAction.setTitle({ title: 'Aktion wählen' }); + } else { + await messenger.messageDisplayAction.setPopup({ popup: '' }); + await messenger.messageDisplayAction.setTitle({ title: actions[0]?.name || 'Erledigt' }); + } +} + +// Update on config change +browser.storage.onChanged.addListener((changes, area) => { + if (area === 'local' && changes.erledigt_config) updateErledigtPopup(); +}); +updateErledigtPopup(); + +// ── Template insertion ── + browser.runtime.onMessage.addListener((msg, sender, sendResponse) => { - if (msg.action !== 'insertTemplate') return false; + if (msg.action === 'erledigtAction') { + (async () => { + try { + const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); + const result = await browser.storage.local.get('erledigt_config'); + const actions = (result.erledigt_config || {}).actions || []; + const action = actions[msg.index] || {}; + await executeErledigtAction(tab, action); + sendResponse({ success: true }); + } catch (e) { + console.error('Erledigt-Action Fehler:', e); + browser.notifications.create({ type: 'basic', iconUrl: browser.runtime.getURL('icons/icon.png'), title: 'Fehler', message: e.message }); + sendResponse({ success: false, error: e.message }); + } + })(); + return true; + } + if (msg.action !== 'insertTemplate') return; handleInsertTemplate(msg).then(() => sendResponse()) .catch(err => sendResponse({ error: err.message })); diff --git a/lib/gitea-sync.js b/lib/gitea-sync.js index 52747d2..e81e8c0 100644 --- a/lib/gitea-sync.js +++ b/lib/gitea-sync.js @@ -8,6 +8,7 @@ const FULL_SYNC_COOLDOWN_MS = 10 * 1000; // min 10s between full pulls const SHARED_FOLDER = '_gemeinsam'; const USER_FOLDER = '_benutzer'; const CONFIG_FOLDER = '_config'; +const SCHLAGWOERTER_CACHE_KEY = 'schlagwoerter_cache'; // ── Gitea API Client ── @@ -146,6 +147,16 @@ class GiteaClient { return null; } } + + async getSchlagwoerter() { + const data = await this.getFile(`${CONFIG_FOLDER}/schlagwoerter.json`); + if (!data) return null; + try { + return JSON.parse(GiteaClient.fromBase64(data.content)); + } catch (_) { + return null; + } + } } // ── Sync Manager ── @@ -545,7 +556,6 @@ class SyncManager { await browser.identities.update(identity.id, { signature: fullSig, signatureIsPlainText: false, - attachSignature: false }); updated++; } @@ -563,7 +573,6 @@ class SyncManager { await browser.identities.update(id, { signature: fullSig, signatureIsPlainText: false, - attachSignature: false }); loadedHeaders[email] = srcHeader; updated++; @@ -685,6 +694,98 @@ class SyncManager { return { success: true }; } + static hslToHex(h, s, l) { + s /= 100; l /= 100; + const a = s * Math.min(l, 1 - l); + const f = n => { const k = (n + h / 30) % 12; return l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1)); }; + const toHex = x => Math.round(x * 255).toString(16).padStart(2, '0'); + return `#${toHex(f(0))}${toHex(f(8))}${toHex(f(4))}`; + } + + /** + * Sync Schlagwörter (tags) from Gitea config to Thunderbird. + * Auto-registers the current user if not yet listed. + * Tags are never removed so they remain traceable forever. + * Format: [ {"name": "Kenny", "color": "#e74c3c"}, ... ] + */ + async syncTags() { + if (!this.isConfigured) return { success: true, created: 0, updated: 0 }; + + const userName = this.config.authorName; + + // Pull current file (may be null if it doesn't exist yet) + const fileData = await this.client.getFile(`${CONFIG_FOLDER}/schlagwoerter.json`); + let schlagwoerter = []; + let fileSha = null; + if (fileData) { + fileSha = fileData.sha; + try { schlagwoerter = JSON.parse(GiteaClient.fromBase64(fileData.content)); } catch (_) {} + } + if (!Array.isArray(schlagwoerter)) schlagwoerter = []; + + // Auto-register current user if not yet listed + let pushed = false; + if (userName) { + const exists = schlagwoerter.some(u => u.name.toLowerCase() === userName.toLowerCase()); + if (!exists) { + const hue = Math.abs([...userName].reduce((h, c) => ((h << 5) - h + c.charCodeAt(0)) | 0, 0)) % 360; + const color = SyncManager.hslToHex(hue, 65, 45); + schlagwoerter.push({ name: userName, color }); + + const json = JSON.stringify(schlagwoerter, null, 2); + const commitMsg = `Schlagwort für ${userName} hinzugefügt`; + if (fileSha) { + await this.client.updateFile(`${CONFIG_FOLDER}/schlagwoerter.json`, json, fileSha, commitMsg); + } else { + await this.client.createFile(`${CONFIG_FOLDER}/schlagwoerter.json`, json, commitMsg); + } + pushed = true; + console.log(`[Sync] Benutzer "${userName}" als Schlagwort registriert`); + } + } + + // Cache for background.js access + await browser.storage.local.set({ [SCHLAGWOERTER_CACHE_KEY]: schlagwoerter }); + + // Create/update Thunderbird tags + const existingTags = await messenger.messages.tags.list(); + const existingByKey = {}; + for (const t of existingTags) existingByKey[t.key] = t; + + let created = 0, updated = 0; + + for (const user of schlagwoerter) { + const key = `$hps_${user.name.toLowerCase().replace(/\s+/g, '_')}`; + const color = user.color || '#999999'; + + if (existingByKey[key]) { + if (existingByKey[key].color !== color) { + await messenger.messages.tags.update(key, { color }); + updated++; + } + } else { + await messenger.messages.tags.create(key, user.name, color); + created++; + } + } + + return { success: true, created, updated, pushed }; + } + + /** + * Get the current user's tag key based on config + */ + getMyTagKey(schlagwoerter) { + if (!this.config || !Array.isArray(schlagwoerter)) return null; + const name = this.config.authorName; + if (!name) return null; + + const match = schlagwoerter.find(u => u.name.toLowerCase() === name.toLowerCase()); + if (!match) return null; + + return `$hps_${match.name.toLowerCase().replace(/\s+/g, '_')}`; + } + async testConnection() { if (!this.isConfigured) throw new Error('Sync nicht konfiguriert'); const repoInfo = await this.client.testConnection(); @@ -699,7 +800,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', 'autoDetect']; + const syncActions = ['testConnection', 'pullTemplates', 'pushTemplates', 'pullSingleTemplate', 'pushSingleTemplate', 'deleteRemoteTemplate', 'listDepartments', 'addDepartment', 'checkRemoteShas', 'pullSignatures', 'pushSignatures', 'loadSignatureTemplate', 'loadFooter', 'pushFooter', 'autoDetect', 'syncTags', 'getMyTagKey']; if (!syncActions.includes(msg.action)) return; try { @@ -717,6 +818,14 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { case 'listDepartments': return await syncManager.listDepartments(); + case 'addDepartment': { + if (!syncManager.isConfigured) throw new Error('Sync nicht konfiguriert'); + const name = msg.name?.trim(); + if (!name) throw new Error('Kein Name angegeben'); + await syncManager.client.createFile(`${name}/.gitkeep`, '', `Abteilung "${name}" angelegt`); + return { success: true }; + } + case 'checkRemoteShas': return await syncManager.checkRemoteShas(); @@ -754,6 +863,15 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { const config = await syncManager.autoDetect(); return { success: true, config }; + case 'syncTags': + return await syncManager.syncTags(); + + case 'getMyTagKey': { + const cached = (await browser.storage.local.get(SCHLAGWOERTER_CACHE_KEY))[SCHLAGWOERTER_CACHE_KEY]; + const tagKey = syncManager.getMyTagKey(cached); + return { success: !!tagKey, tagKey }; + } + default: return { success: false, error: 'Unbekannte Aktion' }; } @@ -767,7 +885,9 @@ browser.runtime.onMessage.addListener(async (msg, sender) => { let lastKnownShas = null; let lastFullSync = 0; +let lastTagSync = 0; let syncInProgress = false; +const TAG_SYNC_INTERVAL_MS = 60 * 1000; const HASH_STORAGE_KEY_BG = 'sync_hashes'; function simpleHashBg(str) { @@ -808,18 +928,30 @@ async function smartSync() { const initialized = await syncManager.init(); if (!initialized) return; - // Lightweight SHA check + syncInProgress = true; + + // Tag sync every 60s (schlagwoerter.json is not in SHA-checked folders) + const now = Date.now(); + if (now - lastTagSync >= TAG_SYNC_INTERVAL_MS) { + lastTagSync = now; + try { + const tagResult = await syncManager.syncTags(); + if (tagResult.created || tagResult.pushed) console.log('[Sync] Tags synchronisiert:', tagResult); + } catch (e) { + console.error('[Sync] Tag-Sync fehlgeschlagen:', e); + } + } + + // Lightweight SHA check for templates + signatures const shaResult = await syncManager.checkRemoteShas(); - if (!shaResult?.success) return; + if (!shaResult?.success) { syncInProgress = false; return; } const currentShas = JSON.stringify(shaResult.remoteShas); // First run or SHAs changed → full pull if (lastKnownShas === null || currentShas !== lastKnownShas) { - const now = Date.now(); - if (now - lastFullSync < FULL_SYNC_COOLDOWN_MS) return; + if (now - lastFullSync < FULL_SYNC_COOLDOWN_MS) { syncInProgress = false; return; } - syncInProgress = true; lastFullSync = now; console.log('[Sync] Änderung erkannt, lade Vorlagen...'); @@ -831,8 +963,8 @@ async function smartSync() { console.log('[Sync] Signaturen geladen:', sigResult); lastKnownShas = JSON.stringify((await syncManager.checkRemoteShas()).remoteShas || {}); - syncInProgress = false; } + syncInProgress = false; } catch (err) { console.error('[Sync] Check fehlgeschlagen:', err); syncInProgress = false; @@ -841,3 +973,4 @@ async function smartSync() { smartSync(); setInterval(smartSync, SHA_CHECK_INTERVAL_MS); + diff --git a/manifest.json b/manifest.json index 24b7bb8..ff4db73 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 2, "name": "HPS Vorlagen & Signaturen", - "version": "2.2.0", + "version": "2.3.0", "description": "Vorlagen- und Signaturverwaltung für Hotel Park Soltau mit Git-Sync", "browser_specific_settings": { "gecko": { @@ -15,7 +15,13 @@ "notifications", "tabs", "accountsRead", - "accountsIdentities" + "accountsIdentities", + "messagesTagsList", + "messagesTags", + "messagesRead", + "messagesUpdate", + "messagesMove", + "accountsFolders" ], "optional_permissions": [ "*://*/*" @@ -24,6 +30,14 @@ "scripts": ["lib/gitea-sync.js", "background.js"], "persistent": true }, + "browser_action": { + "default_icon": { + "16": "icons/icon.png", + "32": "icons/icon.png" + }, + "default_title": "Vorlagen & Signaturen verwalten", + "default_label": "Vorlagen & Signaturen" + }, "compose_action": { "default_icon": { "16": "icons/icon.png", @@ -32,6 +46,13 @@ "default_popup": "popup.html", "default_label": "Vorlagen" }, + "message_display_action": { + "default_icon": { + "16": "icons/icon.png", + "32": "icons/icon.png" + }, + "default_label": "QuickMove" + }, "options_ui": { "page": "templates_options/templates_options.html", "browser_style": true diff --git a/message_popup.html b/message_popup.html new file mode 100644 index 0000000..ac7f16f --- /dev/null +++ b/message_popup.html @@ -0,0 +1,50 @@ + + +
+ +_gemeinsam).