diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 4b56a84..2651c07 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -81,6 +81,26 @@ "message": "Label:", "description": "Label für das Eingabefeld des Webhook-Labels." }, + "optionsEmojiLabel": { + "message": "Emoji (optional):", + "description": "Beschriftung für das Emoji-Eingabefeld." + }, + "optionsEmojiPlaceholder": { + "message": "Emoji wählen, z. B. 🔔", + "description": "Platzhalter für das Emoji-Eingabefeld." + }, + "optionsEmojiChooseButton": { + "message": "Emoji auswählen", + "description": "Schaltflächentext zum Öffnen des Emoji-Pickers." + }, + "optionsEmojiClearButton": { + "message": "Leeren", + "description": "Schaltflächentext zum Leeren des Emoji-Feldes." + }, + "optionsEmojiSearchPlaceholder": { + "message": "Emoji suchen...", + "description": "Platzhalter für das Emoji-Suchfeld." + }, "optionsLabelInputPlaceholder": { "message": "z.B. 'An Slack senden'", "description": "Platzhalter für die Eingabe des Webhook-Labels." @@ -242,7 +262,7 @@ "description": "Hinweistext neben dem Import-Button." }, "optionsTestButton": { - "message": "Testen", + "message": "🧪 Testen", "description": "Text für den Test-Button." }, "optionsTestSuccess": { @@ -252,5 +272,33 @@ "optionsTestError": { "message": "Fehler beim Senden des Test-Webhooks: ", "description": "Fehlermeldung für den Test-Webhook." + }, + "optionsAppearanceHeader": { + "message": "Erscheinungsbild", + "description": "Überschrift für den Bereich Erscheinungsbild." + }, + "optionsThemeLabel": { + "message": "Theme", + "description": "Beschriftung für die Theme-Auswahl." + }, + "optionsThemeSystem": { + "message": "Systemstandard", + "description": "Option für das systemweite Standard-Theme." + }, + "optionsThemeLight": { + "message": "Hell", + "description": "Option für das helle Theme." + }, + "optionsThemeDark": { + "message": "Dunkel", + "description": "Option für das dunkle Theme." + }, + "optionsManageAppearanceButton": { + "message": "Erscheinungsbild", + "description": "Schaltfläche zum Öffnen des Erscheinungsbild-Dialogs." + }, + "optionsCloseAppearanceButton": { + "message": "Schließen", + "description": "Schaltfläche zum Schließen des Erscheinungsbild-Dialogs." } } diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5a48c53..3d28615 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -81,6 +81,26 @@ "message": "Label:", "description": "Label for the webhook label input field." }, + "optionsEmojiLabel": { + "message": "Emoji (optional):", + "description": "Label for the webhook emoji input field." + }, + "optionsEmojiPlaceholder": { + "message": "Pick an emoji, e.g. 🔔", + "description": "Placeholder for the webhook emoji input." + }, + "optionsEmojiChooseButton": { + "message": "Choose Emoji", + "description": "Button text to open the emoji picker." + }, + "optionsEmojiClearButton": { + "message": "Clear", + "description": "Button text to clear the emoji." + }, + "optionsEmojiSearchPlaceholder": { + "message": "Search emoji...", + "description": "Placeholder for the emoji search input." + }, "optionsLabelInputPlaceholder": { "message": "e.g. 'Send to Slack'", "description": "Placeholder for the webhook label input." @@ -242,7 +262,7 @@ "description": "Information text shown next to the import button." }, "optionsTestButton": { - "message": "Test", + "message": "🧪 Test", "description": "Text for the test button." }, "optionsTestSuccess": { @@ -253,4 +273,33 @@ "message": "Error sending test webhook: ", "description": "Error message for the test webhook." } + , + "optionsAppearanceHeader": { + "message": "Appearance", + "description": "Header for the appearance section." + }, + "optionsThemeLabel": { + "message": "Theme", + "description": "Label for the theme selector." + }, + "optionsThemeSystem": { + "message": "System default", + "description": "System default theme option." + }, + "optionsThemeLight": { + "message": "Light", + "description": "Light theme option." + }, + "optionsThemeDark": { + "message": "Dark", + "description": "Dark theme option." + }, + "optionsManageAppearanceButton": { + "message": "Appearance", + "description": "Button to open the appearance modal." + }, + "optionsCloseAppearanceButton": { + "message": "Close", + "description": "Button to close the appearance modal." + } } diff --git a/options/options.css b/options/options.css index 9f50b9d..64538fc 100644 --- a/options/options.css +++ b/options/options.css @@ -277,6 +277,46 @@ button:hover { display: none !important; } +/* Emoji picker */ +.emoji-input-row { + display: flex; + align-items: center; + gap: 8px; +} + +.emoji-picker { + margin-top: 8px; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 12px; + background: #fff; +} + +.emoji-picker input[type="text"] { + margin-bottom: 8px; +} + +.emoji-grid { + display: grid; + grid-template-columns: repeat(8, 1fr); + gap: 6px; +} + +.emoji-item { + font-size: 20px; + line-height: 1; + padding: 6px; + border: 1px solid transparent; + border-radius: 6px; + background: #f9fafb; + cursor: pointer; +} + +.emoji-item:hover { + background: #eef2ff; + border-color: #c7d2fe; +} + #no-webhooks-message { color: #6b7280; padding: 20px; @@ -410,7 +450,7 @@ button:hover { position: relative; } -.close, .close-manage-groups { +.close, .close-manage-groups, .close-manage-appearance { position: absolute; right: 15px; top: 15px; @@ -420,7 +460,7 @@ button:hover { color: #6b7280; } -.close:hover, .close-manage-groups:hover { +.close:hover, .close-manage-groups:hover, .close-manage-appearance:hover { color: #1f2937; } @@ -520,3 +560,44 @@ button:hover { .delete-group:hover { background-color: #dc2626; } + +/* Emoji icons for options buttons (non-intrusive: does not change textContent) */ +#add-new-webhook-btn::before, +#manage-groups-btn::before, +#manage-appearance-btn::before, +#cancel-edit-btn::before, +#close-manage-groups-btn::before, +#close-manage-appearance-btn::before, +#add-webhook-form button[type="submit"]::before, +#add-header-btn::before, +#add-group-btn::before, +#export-webhooks-btn::before, +#import-webhooks-btn::before, +.edit-btn::before, +.duplicate-btn::before, +.delete-btn::before, +.remove-header-btn::before, +.save-group-name::before, +.delete-group::before, +.edit-group-btn::before { + display: inline-block; + margin-right: 6px; +} + +#add-new-webhook-btn::before { content: "➕"; } +#manage-groups-btn::before { content: "🗂️"; } +#add-header-btn::before { content: "➕"; } +#add-group-btn::before { content: "➕"; } +#export-webhooks-btn::before { content: "📤"; } +#import-webhooks-btn::before { content: "📥"; } +#manage-appearance-btn::before { content: "🌓"; } +#add-webhook-form button[type="submit"]::before { content: "💾"; } +#cancel-edit-btn::before { content: "↩️"; } +#close-manage-groups-btn::before, #close-manage-appearance-btn::before { content: "✖️"; } +.edit-btn::before { content: "✏️"; } +.duplicate-btn::before { content: "📄"; } +.delete-btn::before { content: "🗑️"; } +.remove-header-btn::before { content: "❌"; } +.save-group-name::before { content: "💾"; } +.delete-group::before { content: "🗑️"; } +.edit-group-btn::before { content: "✏️"; } diff --git a/options/options.html b/options/options.html index 0a739ed..8a55e52 100644 --- a/options/options.html +++ b/options/options.html @@ -2,6 +2,7 @@ + __MSG_optionsTitle__ @@ -10,6 +11,8 @@

__MSG_optionsPageHeader__

__MSG_optionsPageDescription__

+ + @@ -28,6 +31,23 @@

__MSG_optionsAddWebhookHeader__

required />
+
+ +
+ + + +
+ +
+ + + + +
+ + + + diff --git a/options/options.js b/options/options.js index f161d0d..395c925 100644 --- a/options/options.js +++ b/options/options.js @@ -72,7 +72,7 @@ const loadWebhooks = async () => { const labelSpan = document.createElement("span"); labelSpan.classList.add("label"); - labelSpan.textContent = webhook.label; + labelSpan.textContent = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`; const urlSpan = document.createElement("span"); urlSpan.classList.add("url"); @@ -130,7 +130,7 @@ const loadWebhooks = async () => { const labelSpan = document.createElement("span"); labelSpan.classList.add("label"); - labelSpan.textContent = webhook.label; + labelSpan.textContent = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`; const urlSpan = document.createElement("span"); urlSpan.classList.add("url"); @@ -374,6 +374,7 @@ const labelInput = document.getElementById("webhook-label"); const urlInput = document.getElementById("webhook-url"); const methodSelect = document.getElementById("webhook-method"); const identifierInput = document.getElementById("webhook-identifier"); +const emojiInput = document.getElementById("webhook-emoji"); const headersListDiv = document.getElementById("headers-list"); const headerKeyInput = document.getElementById("header-key"); const headerValueInput = document.getElementById("header-value"); @@ -383,6 +384,7 @@ const showAddWebhookBtn = document.getElementById("add-new-webhook-btn"); const testWebhookBtn = document.getElementById("test-webhook-btn"); const formStatusMessage = document.getElementById("form-status-message"); const manageGroupsBtn = document.getElementById("manage-groups-btn"); +const manageAppearanceBtn = document.getElementById("manage-appearance-btn"); const customPayloadInput = document.getElementById("webhook-custom-payload"); const variablesAutocomplete = document.getElementById("variables-autocomplete"); const toggleCustomPayloadBtn = document.getElementById("toggle-custom-payload"); @@ -395,11 +397,20 @@ const importWebhooksBtn = document.getElementById("import-webhooks-btn"); const importWebhooksInput = document.getElementById("import-webhooks-input"); const groupSelect = document.getElementById("webhook-group"); const manageGroupsModal = document.getElementById("manage-groups-modal"); +const manageAppearanceModal = document.getElementById("manage-appearance-modal"); const newGroupNameInput = document.getElementById("new-group-name"); const addGroupBtn = document.getElementById("add-group-btn"); const groupsList = document.getElementById("groups-list"); const closeManageGroupsBtn = document.getElementById("close-manage-groups-btn"); const closeManageGroups = document.querySelector("#manage-groups-modal .close-manage-groups"); +const closeManageAppearanceBtn = document.getElementById("close-manage-appearance-btn"); +const closeManageAppearance = document.querySelector("#manage-appearance-modal .close-manage-appearance"); +const themeSelect = document.getElementById("theme-select"); +// Emoji picker elements +const openEmojiPickerBtn = document.getElementById('open-emoji-picker-btn'); +const clearEmojiBtn = document.getElementById('clear-emoji-btn'); +const emojiPicker = document.getElementById('emoji-picker'); +const emojiGrid = document.getElementById('emoji-grid'); let headers = []; async function exportWebhooks() { @@ -423,7 +434,9 @@ async function handleImport(event) { const data = JSON.parse(text); const hooks = Array.isArray(data) ? data : data.webhooks; if (Array.isArray(hooks)) { - await saveWebhooks(hooks); + // Backward compatibility: ensure emoji field exists + const normalized = hooks.map(h => ({ ...h, emoji: h.emoji || "" })); + await saveWebhooks(normalized); loadWebhooks(); } } catch (e) { @@ -456,8 +469,10 @@ const importData = async (file) => { reader.onload = async (event) => { try { const data = JSON.parse(event.target.result); - const webhooks = Array.isArray(data.webhooks) ? data.webhooks : []; + let webhooks = Array.isArray(data.webhooks) ? data.webhooks : []; const groups = Array.isArray(data.groups) ? data.groups : []; + // Ensure emoji field exists + webhooks = webhooks.map(h => ({ ...h, emoji: h.emoji || "" })); await browser.storage.sync.set({ webhooks, groups }); await loadWebhooks(); if (typeof renderGroups === 'function') await renderGroups(); @@ -496,8 +511,10 @@ if (importWebhooksBtn && importWebhooksInput) { reader.onload = async (event) => { try { const data = JSON.parse(event.target.result); - const webhooks = Array.isArray(data.webhooks) ? data.webhooks : []; + let webhooks = Array.isArray(data.webhooks) ? data.webhooks : []; const groups = Array.isArray(data.groups) ? data.groups : []; + // Ensure emoji field exists + webhooks = webhooks.map(h => ({ ...h, emoji: h.emoji || "" })); await browser.storage.sync.set({ webhooks, groups }); await loadWebhooks(); if (typeof renderGroups === 'function') await renderGroups(); @@ -528,6 +545,16 @@ manageGroupsBtn.addEventListener('click', async () => { await renderGroups(); }); +// Manage appearance button +if (manageAppearanceBtn) { + manageAppearanceBtn.addEventListener('click', async () => { + manageAppearanceModal.classList.remove('hidden'); + // Initialize theme select when opening + const { theme } = await browser.storage.sync.get('theme'); + themeSelect.value = theme || 'system'; + }); +} + // Close modal functions const closeManageGroupsModalFunc = () => { manageGroupsModal.classList.add('hidden'); @@ -537,6 +564,13 @@ const closeManageGroupsModalFunc = () => { closeManageGroupsBtn.addEventListener('click', closeManageGroupsModalFunc); closeManageGroups.addEventListener('click', closeManageGroupsModalFunc); +// Close appearance modal +const closeManageAppearanceModalFunc = () => { + manageAppearanceModal.classList.add('hidden'); +}; +if (closeManageAppearanceBtn) closeManageAppearanceBtn.addEventListener('click', closeManageAppearanceModalFunc); +if (closeManageAppearance) closeManageAppearance.addEventListener('click', closeManageAppearanceModalFunc); + // Close modals when clicking outside manageGroupsModal.addEventListener('click', (e) => { if (e.target === manageGroupsModal) { @@ -544,6 +578,15 @@ manageGroupsModal.addEventListener('click', (e) => { } }); +// Close appearance modal when clicking outside +if (manageAppearanceModal) { + manageAppearanceModal.addEventListener('click', (e) => { + if (e.target === manageAppearanceModal) { + closeManageAppearanceModalFunc(); + } + }); +} + // Add group addGroupBtn.addEventListener('click', async () => { const groupName = newGroupNameInput.value.trim(); @@ -751,6 +794,7 @@ form.addEventListener("submit", async (e) => { const url = urlInput.value.trim(); const method = methodSelect.value; const identifier = identifierInput.value.trim(); + const emoji = emojiInput ? emojiInput.value.trim() : ""; const urlFilter = urlFilterInput.value.trim(); const customPayload = customPayloadInput.value.trim(); const groupId = groupSelect.value || null; @@ -768,7 +812,8 @@ form.addEventListener("submit", async (e) => { identifier, customPayload: customPayload || null, urlFilter: urlFilter || "", - groupId + groupId, + emoji: emoji || "" } : wh ); editWebhookId = null; @@ -784,7 +829,8 @@ form.addEventListener("submit", async (e) => { identifier, customPayload: customPayload || null, urlFilter: urlFilter || "", - groupId + groupId, + emoji: emoji || "" }; webhooks.push(newWebhook); } @@ -794,6 +840,7 @@ form.addEventListener("submit", async (e) => { urlInput.value = ""; methodSelect.value = "POST"; identifierInput.value = ""; + if (emojiInput) emojiInput.value = ""; urlFilterInput.value = ""; customPayloadInput.value = ""; headerKeyInput.value = ""; @@ -892,6 +939,7 @@ webhookList.addEventListener("click", async (e) => { urlFilterInput.value = webhook.urlFilter || ""; customPayloadInput.value = webhook.customPayload || ""; groupSelect.value = webhook.groupId || ""; + if (emojiInput) emojiInput.value = webhook.emoji || ""; headers = Array.isArray(webhook.headers) ? [...webhook.headers] : []; renderHeaders(); cancelEditBtn.classList.remove("hidden"); @@ -918,6 +966,7 @@ webhookList.addEventListener("click", async (e) => { urlFilterInput.value = webhook.urlFilter || ""; customPayloadInput.value = webhook.customPayload || ""; groupSelect.value = webhook.groupId || ""; + if (emojiInput) emojiInput.value = webhook.emoji || ""; headers = Array.isArray(webhook.headers) ? [...webhook.headers] : []; renderHeaders(); cancelEditBtn.classList.remove("hidden"); @@ -944,6 +993,7 @@ cancelEditBtn.addEventListener("click", () => { headerKeyInput.value = ""; headerValueInput.value = ""; groupSelect.value = ""; + if (emojiInput) emojiInput.value = ""; headers = []; renderHeaders(); cancelEditBtn.classList.add("hidden"); @@ -1072,6 +1122,59 @@ document.addEventListener("DOMContentLoaded", () => { // Load webhooks loadWebhooks(); + + // Initialize theme select + if (themeSelect) { + browser.storage.sync.get("theme").then((res) => { + const value = res && res.theme ? res.theme : "system"; + themeSelect.value = value; + }); + + themeSelect.addEventListener("change", () => { + const value = themeSelect.value; + browser.storage.sync.set({ theme: value }); + }); + } + + // Initialize emoji picker if present + if (openEmojiPickerBtn && emojiPicker && emojiGrid && emojiInput) { + const EMOJIS = [ + '🔔','✅','🚀','⚠️','❗','ℹ️','⭐','🔥','🛠️','🧪','📦','📣','📝','🧰','💡','🖥️','📱','🌐','🔒','🔑','🛡️','💾','📤','📥','🔄','⏱️','🕒','🧭','📊','📈','🧹','🧼','🧯','🧩','🧠','🛎️','🚨','🎉','🧵','🔍','🧰','🔗','✉️','📫','📬','🛰️','🌟','🎯','🪄','🧲','🧪','🧱','🪵' + ]; + + function renderEmojiGrid() { + emojiGrid.textContent = ''; + EMOJIS.forEach(e => { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'emoji-item'; + btn.textContent = e; + btn.addEventListener('click', () => { + emojiInput.value = e; + emojiPicker.classList.add('hidden'); + emojiInput.focus(); + }); + emojiGrid.appendChild(btn); + }); + } + + openEmojiPickerBtn.addEventListener('click', () => { + if (emojiPicker.classList.contains('hidden')) { + renderEmojiGrid(); + emojiPicker.classList.remove('hidden'); + } else { + emojiPicker.classList.add('hidden'); + } + }); + + if (clearEmojiBtn) { + clearEmojiBtn.addEventListener('click', () => { + emojiInput.value = ''; + emojiInput.focus(); + }); + } + + } }); // Export functions for testing in Node environment diff --git a/popup/popup.css b/popup/popup.css index b73b68e..a3bf0d5 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -1,107 +1,181 @@ +/* Theme variables */ +:root { + --bg: #f7f8fa; + --card-bg: #ffffff; + --text: #1f2937; + --muted: #6b7280; + --primary: #2563eb; + --primary-contrast: #ffffff; + --border: #e5e7eb; + --header-grad: linear-gradient(135deg, #6366f1, #2563eb); + --btn-bg: linear-gradient(135deg, #4f46e5, #2563eb); + --btn-bg-hover: linear-gradient(135deg, #4338ca, #1d4ed8); + --success-bg: #dcfce7; + --success-text: #166534; + --error-bg: #fee2e2; + --error-text: #991b1b; + --shadow: 0 6px 18px rgba(0, 0, 0, 0.08); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0b0f14; + --card-bg: #0f172a; + --text: #e5e7eb; + --muted: #9ca3af; + --primary: #60a5fa; + --primary-contrast: #0b0f14; + --border: #223045; + --header-grad: linear-gradient(135deg, #0ea5e9, #6366f1); + --btn-bg: linear-gradient(135deg, #0284c7, #4f46e5); + --btn-bg-hover: linear-gradient(135deg, #0369a1, #4338ca); + --success-bg: #064e3b; + --success-text: #a7f3d0; + --error-bg: #7f1d1d; + --error-text: #fecaca; + --shadow: 0 8px 20px rgba(0, 0, 0, 0.35); + } +} + +/* Explicit overrides */ +[data-theme="light"] { + --bg: #f7f8fa; + --card-bg: #ffffff; + --text: #1f2937; + --muted: #6b7280; + --primary: #2563eb; + --primary-contrast: #ffffff; + --border: #e5e7eb; + --header-grad: linear-gradient(135deg, #6366f1, #2563eb); + --btn-bg: linear-gradient(135deg, #4f46e5, #2563eb); + --btn-bg-hover: linear-gradient(135deg, #4338ca, #1d4ed8); + --success-bg: #dcfce7; + --success-text: #166534; + --error-bg: #fee2e2; + --error-text: #991b1b; + --shadow: 0 6px 18px rgba(0, 0, 0, 0.08); +} + +[data-theme="dark"] { + --bg: #0b0f14; + --card-bg: #0f172a; + --text: #e5e7eb; + --muted: #9ca3af; + --primary: #60a5fa; + --primary-contrast: #0b0f14; + --border: #223045; + --header-grad: linear-gradient(135deg, #0ea5e9, #6366f1); + --btn-bg: linear-gradient(135deg, #0284c7, #4f46e5); + --btn-bg-hover: linear-gradient(135deg, #0369a1, #4338ca); + --success-bg: #064e3b; + --success-text: #a7f3d0; + --error-bg: #7f1d1d; + --error-text: #fecaca; + --shadow: 0 8px 20px rgba(0, 0, 0, 0.35); +} + +/* Base layout */ body { - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, - sans-serif; - width: 300px; - padding: 0; - margin: 0; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, + sans-serif; + width: 320px; + padding: 0; + margin: 0; + background: var(--bg); + color: var(--text); } .container { - padding: 15px; -} - -.header { - text-align: center; - border-bottom: 1px solid #ccc; - padding-bottom: 10px; - margin-bottom: 15px; -} - -.header h3 { - margin: 0; - color: #333; + padding: 14px 14px 12px 14px; + background: transparent; + box-shadow: none; } #buttons-container { - display: flex; - flex-direction: column; - gap: 10px; + display: flex; + flex-direction: column; + gap: 8px; } .group-header { - font-weight: bold; - color: #555; - margin-top: 10px; - margin-bottom: 5px; - padding-bottom: 5px; - border-bottom: 1px solid #eee; + display: block; + font-weight: 700; + color: var(--text); + margin: 16px 0 6px 0; + padding-left: 10px; + border-left: 3px solid var(--primary); + font-size: 13px; + letter-spacing: 0.2px; } .webhook-btn { - width: 100%; - padding: 12px; - font-size: 1em; - color: #fff; - background-color: #007bff; - border: none; - border-radius: 5px; - cursor: pointer; - transition: - background-color 0.2s ease-in-out, - transform 0.1s ease; + width: 100%; + padding: 12px 12px; + font-size: 14px; + color: var(--primary-contrast); + background: var(--btn-bg); + border: 1px solid transparent; + border-radius: 10px; + cursor: pointer; + transition: transform 0.06s ease, box-shadow 0.2s ease, background 0.2s ease; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); } + .webhook-btn:hover { - background-color: #0056b3; + background: var(--btn-bg-hover); + box-shadow: 0 4px 10px rgba(0,0,0,0.15); } .webhook-btn:active { - transform: scale(0.98); + transform: translateY(1px); } .webhook-btn:disabled { - background-color: #6c757d; - cursor: not-allowed; + opacity: 0.6; + cursor: not-allowed; } .no-hooks-msg { - color: #666; - text-align: center; - padding: 10px; + color: var(--muted); + text-align: center; + padding: 10px; } #status-message { - margin-top: 15px; - padding: 8px; - text-align: center; - border-radius: 4px; - font-size: 0.9em; + margin-top: 12px; + padding: 8px 10px; + text-align: center; + border-radius: 8px; + font-size: 12px; + border: 1px solid var(--border); } #status-message.success { - background-color: #d4edda; - color: #155724; + background-color: var(--success-bg); + color: var(--success-text); + border-color: transparent; } #status-message.error { - background-color: #f8d7da; - color: #721c24; + background-color: var(--error-bg); + color: var(--error-text); + border-color: transparent; } .footer { - margin-top: 15px; - padding-top: 10px; - border-top: 1px solid #ccc; - text-align: center; + margin-top: 12px; + padding-top: 2px; + text-align: center; } .footer a { - color: #007bff; - text-decoration: none; - font-size: 0.9em; + color: var(--primary); + text-decoration: none; + font-size: 12px; } .footer a:hover { - text-decoration: underline; + text-decoration: underline; } diff --git a/popup/popup.html b/popup/popup.html index d2325c3..e030844 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -2,14 +2,12 @@ + __MSG_popupTitle__
-
-

__MSG_popupHeader__

-
diff --git a/popup/popup.js b/popup/popup.js index e6c0e53..e53f944 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -7,6 +7,20 @@ document.addEventListener("DOMContentLoaded", async () => { const buttonsContainer = document.getElementById("buttons-container"); + // Apply theme preference (system | light | dark) + try { + const themeResult = await browserAPI.storage.sync.get("theme"); + const theme = themeResult && themeResult.theme ? themeResult.theme : "system"; + const root = document.documentElement; + if (theme === "light" || theme === "dark") { + root.setAttribute("data-theme", theme); + } else { + root.removeAttribute("data-theme"); + } + } catch (_) { + // ignore theme errors, fallback to system + } + // Load webhooks and groups from storage const { webhooks = [], groups = [] } = await browserAPI.storage.sync.get(["webhooks", "groups"]); const tabs = await browserAPI.tabs.query({ active: true, currentWindow: true }); @@ -51,9 +65,10 @@ document.addEventListener("DOMContentLoaded", async () => { // Create a button for each webhook in this group groupWebhooks.forEach((webhook) => { const button = document.createElement("button"); - button.textContent = webhook.label; + const displayLabel = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`; + button.textContent = displayLabel; button.dataset.url = webhook.url; - button.dataset.label = webhook.label; + button.dataset.label = displayLabel; button.dataset.webhookId = webhook.id; button.classList.add("webhook-btn"); buttonsContainer.appendChild(button); @@ -73,9 +88,10 @@ document.addEventListener("DOMContentLoaded", async () => { // Create a button for each ungrouped webhook ungroupedWebhooks.forEach((webhook) => { const button = document.createElement("button"); - button.textContent = webhook.label; + const displayLabel = `${webhook.emoji ? webhook.emoji + ' ' : ''}${webhook.label}`; + button.textContent = displayLabel; button.dataset.url = webhook.url; - button.dataset.label = webhook.label; + button.dataset.label = displayLabel; button.dataset.webhookId = webhook.id; button.classList.add("webhook-btn"); buttonsContainer.appendChild(button); diff --git a/tests/exportImport.test.js b/tests/exportImport.test.js index ecabcaf..9f551c9 100644 --- a/tests/exportImport.test.js +++ b/tests/exportImport.test.js @@ -155,7 +155,7 @@ describe('export and import logic', () => { await handleImport(event); - expect(global.browser.storage.sync.set).toHaveBeenCalledWith({ webhooks: hooks }); + expect(global.browser.storage.sync.set).toHaveBeenCalledWith({ webhooks: hooks.map(h => ({...h, emoji: ''})) }); expect(event.target.value).toBe(''); }); }); diff --git a/tests/options.test.js b/tests/options.test.js index c348b90..2a0c08f 100644 --- a/tests/options.test.js +++ b/tests/options.test.js @@ -208,7 +208,8 @@ describe('options page', () => { identifier: 'test-identifier', customPayload, urlFilter: 'example.com', - groupId: null + groupId: null, + emoji: '' }] }); }); diff --git a/utils/utils.js b/utils/utils.js index 79e9635..98dd95d 100644 --- a/utils/utils.js +++ b/utils/utils.js @@ -32,6 +32,15 @@ const getBrowserAPI = () => { function replaceI18nPlaceholders() { const browserAPI = getBrowserAPI(); + // Helper to replace __MSG_key__ tokens within a string + const replaceTokens = (value) => { + if (!value || typeof value !== 'string' || !value.includes('__MSG_')) return value; + return value.replace(/__MSG_([^_]+)__/g, (match, key) => { + const msg = browserAPI.i18n?.getMessage ? browserAPI.i18n.getMessage(key) : ''; + return msg || match; + }); + }; + // Replace textContent of elements with data-i18n attribute const elements = document.querySelectorAll('[data-i18n]'); elements.forEach(element => { @@ -49,37 +58,28 @@ function replaceI18nPlaceholders() { if (element.childNodes && element.childNodes.length > 0) { element.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE && node.nodeValue && node.nodeValue.includes('__MSG_')) { - const matches = node.nodeValue.match(/__MSG_([^_]+)__/g); - if (matches) { - let newValue = node.nodeValue; - matches.forEach(match => { - const key = match.replace('__MSG_', '').replace('__', ''); - const message = browserAPI.i18n.getMessage(key); - if (message) { - newValue = newValue.replace(match, message); - } - }); - node.nodeValue = newValue; - } + node.nodeValue = replaceTokens(node.nodeValue); } }); } }); + // Replace __MSG_...__ patterns in common attributes (e.g., placeholder, title, aria-label) + document.querySelectorAll('*').forEach(el => { + if (!el.attributes) return; + Array.from(el.attributes).forEach(attr => { + if (typeof attr.value === 'string' && attr.value.includes('__MSG_')) { + const newVal = replaceTokens(attr.value); + if (newVal !== attr.value) { + el.setAttribute(attr.name, newVal); + } + } + }); + }); + // Replace __MSG_...__ patterns in the document title if (document.title && document.title.includes('__MSG_')) { - const matches = document.title.match(/__MSG_([^_]+)__/g); - if (matches) { - let newTitle = document.title; - matches.forEach(match => { - const key = match.replace('__MSG_', '').replace('__', ''); - const message = browserAPI.i18n.getMessage(key); - if (message) { - newTitle = newTitle.replace(match, message); - } - }); - document.title = newTitle; - } + document.title = replaceTokens(document.title); } }