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 @@ + + + + + Aktion wählen + + + +
+ + + diff --git a/message_popup.js b/message_popup.js new file mode 100644 index 0000000..3565a72 --- /dev/null +++ b/message_popup.js @@ -0,0 +1,21 @@ +(async () => { + const result = await browser.storage.local.get('erledigt_config'); + const actions = (result.erledigt_config || {}).actions || []; + const list = document.getElementById('action-list'); + + if (actions.length === 0) { + list.innerHTML = '
Keine Aktionen konfiguriert.
'; + return; + } + + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + const btn = document.createElement('button'); + btn.textContent = action.name || `Aktion ${i + 1}`; + btn.addEventListener('click', async () => { + await browser.runtime.sendMessage({ action: 'erledigtAction', index: i }); + window.close(); + }); + list.appendChild(btn); + } +})(); diff --git a/templates-reply-hotel.xpi b/templates-reply-hotel.xpi index 1d4f9ae..8409787 100644 Binary files a/templates-reply-hotel.xpi and b/templates-reply-hotel.xpi differ diff --git a/templates_options/templates_options.html b/templates_options/templates_options.html index ad92766..bf45822 100644 --- a/templates_options/templates_options.html +++ b/templates_options/templates_options.html @@ -731,6 +731,7 @@
+
@@ -990,6 +991,7 @@ +
Du erhältst Vorlagen aus deiner Abteilung + dem gemeinsamen Ordner (_gemeinsam).
@@ -1044,6 +1046,24 @@
+ +
+
+
Aktionen
+
Der Button erscheint in der Nachrichtenansicht. Ein Klick markiert die E-Mail mit deinem Schlagwort und verschiebt sie in den gewählten Ordner. Bei mehreren Aktionen öffnet sich ein Auswahlmenü.
+ +
+ +
+ +
+ +
+ +
+
+
+
Keine Internetverbindung – Synchronisation nicht möglich
diff --git a/templates_options/templates_options.js b/templates_options/templates_options.js index 7f63fd4..6edc994 100644 --- a/templates_options/templates_options.js +++ b/templates_options/templates_options.js @@ -1167,6 +1167,7 @@ function showSigStatus(message, color) { // Save signature to Thunderbird identity (header + footer) document.getElementById('sig-save-button').addEventListener('click', async () => { + try { const identityId = sigIdentitySelect.value; if (!identityId) { showSigStatus('Bitte Identität auswählen.', 'red'); @@ -1194,7 +1195,6 @@ document.getElementById('sig-save-button').addEventListener('click', async () => await browser.identities.update(identityId, { signature: fullSignature, signatureIsPlainText: false, - attachSignature: false }); if (identity) identity.signature = fullSignature; @@ -1206,28 +1206,37 @@ document.getElementById('sig-save-button').addEventListener('click', async () => await browser.identities.update(otherId.id, { signature: fullSignature, signatureIsPlainText: false, - attachSignature: false - }); + }); otherId.signature = fullSignature; } } updateSigSyncIndicator(); - // Auto-push to server + // Auto-push to server, then pull to resolve reference chains try { - const pushResult = await browser.runtime.sendMessage({ action: 'pushSignatures' }); - await loadIdentities(); - for (const id of allIdentities) { - sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || ''); - } - saveSyncHashes(); - updateSigSyncIndicator(); - const label = source.startsWith('=') ? ` (von ${source.substring(1)})` : ''; - showSigStatus(`Signatur gespeichert & hochgeladen!${label}`, 'green'); + await browser.runtime.sendMessage({ action: 'pushSignatures' }); } catch (err) { showSigStatus('Gespeichert, aber Upload fehlgeschlagen: ' + err.message, '#e65100'); + return; } + + try { + await browser.runtime.sendMessage({ action: 'pullSignatures' }); + } catch (_) {} + + await loadIdentities(); + for (const id of allIdentities) { + sigSyncedHashes[id.email.toLowerCase()] = simpleHash(id.signature || ''); + } + saveSyncHashes(); + updateSigSyncIndicator(); + const label = source.startsWith('=') ? ` (von ${source.substring(1)})` : ''; + showSigStatus(`Signatur gespeichert & hochgeladen!${label}`, 'green'); + } catch (err) { + console.error('Signatur-Speichern Fehler:', err); + showSigStatus('Fehler: ' + err.message, 'red'); + } }); // Import signature from HTML file @@ -1399,6 +1408,30 @@ async function populateEmailDropdown() { } } +async function populateFolderDropdown() { + try { + let options = ''; + const accounts = await browser.accounts.list(); + for (const account of accounts) { + const folders = await browser.folders.getSubFolders(account); + const addFolders = (list, prefix) => { + for (const folder of list) { + const label = prefix ? `${prefix} / ${folder.name}` : `${account.name} / ${folder.name}`; + const val = JSON.stringify({ accountId: account.id, path: folder.path }); + options += ``; + if (folder.subFolders && folder.subFolders.length) { + addFolders(folder.subFolders, label); + } + } + }; + addFolders(folders, ''); + } + erledigtFolderOptions = options; + } catch (e) { + console.error('Ordner laden fehlgeschlagen:', e); + } +} + async function loadSyncConfig() { try { const result = await browser.storage.local.get(SYNC_CONFIG_KEY); @@ -1651,6 +1684,23 @@ for (const id of ['sync-author-name', 'sync-author-email']) { document.getElementById('refresh-departments').addEventListener('click', loadDepartments); +document.getElementById('add-department').addEventListener('click', async () => { + const name = prompt('Name der neuen Abteilung:'); + if (!name || !name.trim()) return; + try { + const result = await browser.runtime.sendMessage({ action: 'addDepartment', name: name.trim() }); + if (result?.success) { + showToast(`Abteilung "${name.trim()}" angelegt!`, 'success'); + await loadDepartments(); + document.getElementById('sync-department').value = name.trim(); + } else { + showToast(result?.error || 'Fehler beim Anlegen', 'error'); + } + } catch (e) { + showToast('Fehler: ' + e.message, 'error'); + } +}); + document.getElementById('test-sync-connection').addEventListener('click', async () => { if (!checkOnline()) return; const btn = document.getElementById('test-sync-connection'); @@ -1678,6 +1728,80 @@ document.getElementById('test-sync-connection').addEventListener('click', async btn.textContent = origText; }); +// ── Erledigt-Tab ── + +const ERLEDIGT_CONFIG_KEY = 'erledigt_config'; +let erledigtFolderOptions = ''; + +function renderErledigtAction(action, index) { + const div = document.createElement('div'); + div.className = 'form-row'; + div.style.alignItems = 'end'; + div.style.marginBottom = '8px'; + div.innerHTML = ` +
+ ${index === 0 ? '' : ''} + +
+
+ ${index === 0 ? '' : ''} + +
+ + `; + if (action.targetFolder) { + div.querySelector('.erledigt-folder').value = action.targetFolder; + } + div.querySelector('.erledigt-remove').addEventListener('click', () => { + div.remove(); + }); + return div; +} + +function renderErledigtActions(actions) { + const list = document.getElementById('erledigt-actions-list'); + list.innerHTML = ''; + actions.forEach((a, i) => list.appendChild(renderErledigtAction(a, i))); +} + +function getErledigtActionsFromForm() { + const actions = []; + const names = document.querySelectorAll('.erledigt-name'); + const folders = document.querySelectorAll('.erledigt-folder'); + for (let i = 0; i < names.length; i++) { + const name = names[i].value.trim(); + if (!name) continue; + actions.push({ name, targetFolder: folders[i].value || '' }); + } + return actions; +} + +async function loadErledigtConfig() { + const result = await browser.storage.local.get(ERLEDIGT_CONFIG_KEY); + const config = result[ERLEDIGT_CONFIG_KEY] || {}; + const actions = config.actions || []; + if (actions.length === 0) actions.push({ name: 'Erledigt', targetFolder: '' }); + renderErledigtActions(actions); +} + +document.getElementById('add-erledigt-action').addEventListener('click', () => { + const list = document.getElementById('erledigt-actions-list'); + list.appendChild(renderErledigtAction({ name: '', targetFolder: '' }, list.children.length)); +}); + +document.getElementById('save-erledigt-config').addEventListener('click', async () => { + const actions = getErledigtActionsFromForm(); + if (actions.length === 0) { + showToast('Mindestens eine Aktion mit Namen anlegen.', 'error'); + return; + } + await browser.storage.local.set({ [ERLEDIGT_CONFIG_KEY]: { actions } }); + showToast('Aktionen gespeichert!', 'success'); +}); + // ── Init ── window.addEventListener('load', async () => { @@ -1686,6 +1810,8 @@ window.addEventListener('load', async () => { renderTemplates(templates); updateTplSyncIndicator(); await populateEmailDropdown(); + await populateFolderDropdown(); + await loadErledigtConfig(); loadSyncConfig(); await loadIdentities(); diff --git a/toolbar_popup.html b/toolbar_popup.html new file mode 100644 index 0000000..0520f1f --- /dev/null +++ b/toolbar_popup.html @@ -0,0 +1,9 @@ + + + + + +