From 401ae6b8eab1240372d8f5f4295674496d2bc3a1 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Sep 2024 23:55:27 +0200 Subject: [PATCH] working on a shortcut that opens a popup. see #130 --- _locales/en/messages.json | 4 + js/mzta-menus.js | 41 +++++++- js/mzta-store.js | 32 ++++++ manifest.json | 11 +- mzta-background.js | 49 +++++++++ popup/mzta-popup.css | 94 +++++++++++++++++ popup/mzta-popup.html | 20 ++++ popup/mzta-popup.js | 212 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 458 insertions(+), 5 deletions(-) create mode 100644 js/mzta-store.js create mode 100644 popup/mzta-popup.css create mode 100644 popup/mzta-popup.html create mode 100644 popup/mzta-popup.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6d2347ee..3ccb5f74 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -542,5 +542,9 @@ "StorageSpace": { "message": "Total storage space occupied", "description": "" + }, + "SearchPrompt": { + "message": "Search a prompt", + "description": "" } } \ No newline at end of file diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 9b709e19..a6c8fd96 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -19,7 +19,7 @@ // Some original methods are derived from https://github.com/ali-raheem/Aify/blob/cfadf52f576b7be3720b5b73af7c8d3129c054da/plugin/html/actions.js import { getPrompts } from './mzta-prompts.js'; -import { getLanguageDisplayName, getMenuContextCompose, getMenuContextDisplay } from './mzta-utils.js' +import { getLanguageDisplayName, getMenuContextCompose, getMenuContextDisplay, i18nConditionalGet } from './mzta-utils.js' export class mzta_Menus { @@ -33,6 +33,10 @@ export class mzta_Menus { //{ id: 'ItemC', act: (info, tab) => { console.log('ItemC', info, tab, info.menuItemId); alert('ItemC') } }, ]; + shortcutMenu = [ + //{ id: 'ItemD', label: 'LabelD' }, + ]; + constructor(openChatGPT) { this.menu_context_compose = getMenuContextCompose(); this.menu_context_display = getMenuContextDisplay(); @@ -45,6 +49,7 @@ export class mzta_Menus { async initialize() { this.allPrompts = []; this.rootMenu = []; + this.shortcutMenu = []; this.menu_listeners = {}; this.allPrompts = await getPrompts(true); this.allPrompts.sort((a, b) => a.name.localeCompare(b.name)); @@ -55,7 +60,7 @@ export class mzta_Menus { async reload(){ await browser.menus.removeAll().catch(error => { - console.error("[ThunderAI] ERROR removing the menus: ", error); + console.error("[ThunderAI] ERROR removing the menus: ", error); }); this.removeClickListener(); this.loadMenus(); @@ -140,10 +145,23 @@ export class mzta_Menus { } } + loadShortcutMenu() { + this.shortcutMenu = []; + this.allPrompts.forEach((prompt) => { + this.addShortcutMenu(prompt); + }); + } + + addShortcutMenu(prompt) { + let curr_menu_entry = {id: prompt.id, label: i18nConditionalGet(prompt.name), type: prompt.type}; + this.shortcutMenu.push(curr_menu_entry); + } + async loadMenus() { await this.initialize(); await this.addMenu(this.rootMenu); this.addClickListener(); + this.loadShortcutMenu(); } listener(info, tab) { @@ -219,4 +237,23 @@ export class mzta_Menus { } } + + async executeMenuAction(id) { + // Retrieve the action callback from the menu listeners using the provided ID + const action = this.menu_listeners[id]; + + if (action) { + try { + // Execute the action callback + await action(); + } catch (error) { + // Log any errors that occur during execution + console.error(`Error executing action for menu item ${id}:`, error); + } + } else { + // Warn if no action is found for the provided ID + console.warn(`No action found for menu item ID: ${id}`); + } + } + } \ No newline at end of file diff --git a/js/mzta-store.js b/js/mzta-store.js new file mode 100644 index 00000000..1a12b15a --- /dev/null +++ b/js/mzta-store.js @@ -0,0 +1,32 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +export const taStore = { + + async setSessionData (key, value) { + let obj = {}; + obj[key] = value; + await browser.storage.session.set(obj); + }, + + async getSessionData (key) { + let output = await browser.storage.session.get(key); + return output[key]; + }, +}; \ No newline at end of file diff --git a/manifest.json b/manifest.json index d1f88f32..ebd1464b 100644 --- a/manifest.json +++ b/manifest.json @@ -14,12 +14,12 @@ "message_display_action": { "default_title": "__MSG_menu_title__", "default_icon": "images/icon-32px.png", - "type": "menu" + "default_popup": "popup/mzta-popup.html" }, "compose_action": { "default_title": "__MSG_menu_title__", "default_icon": "images/icon-32px.png", - "type":"menu" + "default_popup": "popup/mzta-popup.html" }, "permissions": [ "compose", @@ -44,5 +44,10 @@ "default_locale": "en", "options_ui": { "page": "options/mzta-options.html" - } + }, + "commands": { + "_thunderai__do_action": { + "description": "" + } + } } diff --git a/mzta-background.js b/mzta-background.js index 49763d55..f7d35795 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -20,6 +20,7 @@ import { mzta_script } from './js/mzta-chatgpt.js'; import { prefs_default } from './options/mzta-options-default.js'; import { mzta_Menus } from './js/mzta-menus.js'; import { taLogger } from './js/mzta-logger.js'; +import { taStore } from './js/mzta-store.js'; import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, generateCallID, migrateCustomPromptsStorage, migrateDefaultPromptsPropStorage } from './js/mzta-utils.js'; await migrateCustomPromptsStorage(); @@ -65,6 +66,50 @@ browser.contentScripts.register({ runAt: "document_idle" }); +let ThunderAI_Shortcut = "Ctrl+Alt+A"; + +// Shortcut +messenger.commands.update({ + name: "_thunderai__do_action", + shortcut: ThunderAI_Shortcut +}).then(() => { + taLog.log('Shortcut [' + ThunderAI_Shortcut + '] registered successfully!'); +}).catch((error) => { + taLog.error('Error registering shortcut [' + ThunderAI_Shortcut + ']: ' + error); +}); + +// Listen for shortcut command +messenger.commands.onCommand.addListener((command, tab) => { + if (command === "_thunderai__do_action") { + handleShortcut(tab); + } +}); + +async function handleShortcut(tab) { + taLog.log("Shortcut triggered!"); + if(!["mail", "messageCompose","messageDisplay"].includes(tab.type)){ + return; + } + await taStore.setSessionData("lastShortcutTabId", tab.id); + await taStore.setSessionData("lastShortcutTabType", tab.type); + await taStore.setSessionData("lastShortcutPromptsData", menus.shortcutMenu); + console.log(">>>>>>>> menus.shortcutMenu: " + JSON.stringify(menus.shortcutMenu)); + switch (tab.type) { + case "mail": + case "messageDisplay": + taStore.setSessionData("lastShortcutFiltering", 1); + browser.messageDisplayAction.openPopup(); + break; + case "messageCompose": + taStore.setSessionData("lastShortcutFiltering", 2); + browser.composeAction.openPopup(); + break; + + default: + break; + } +} + messenger.runtime.onMessage.addListener(async (message, sender, sendResponse) => { // Check what type of message we have received and invoke the appropriate // handler function. @@ -147,6 +192,10 @@ messenger.runtime.onMessage.addListener(async (message, sender, sendResponse) => await menus.reload(); taLog.log("[ThunderAI] Reloaded menus"); break; + case 'shortcut_do_prompt': + taLog.log("Executing shortcut, promptId: " + message.promptId); + menus.executeMenuAction(message.promptId); + brea default: break; } diff --git a/popup/mzta-popup.css b/popup/mzta-popup.css new file mode 100644 index 00000000..6f1ef3de --- /dev/null +++ b/popup/mzta-popup.css @@ -0,0 +1,94 @@ +body { + background-color: rgb(122, 180, 255); +} + +#mzta_popup{ + width: 10em; + height: 11em; + background-color: rgb(122, 180, 255); + padding: 0; +} + +#mzta_search_banner{ + background-color: rgb(122, 180, 255); + color:black; + border-radius: 5px; + width: fit-content; + height: fit-content; + display: flex; + justify-content: center; + align-items: center; + position: relative; + width: fit-content; + height: 2em; + z-index: 1000; + } + + #mzta_search_input{ + width: 12em; + } + + #mzta_autocomplete-items { + position: absolute; + top: 2em; + left: 0em; + border: 1px solid #ccc; + border-top: none; + z-index: 1001; + background-color: #fff; + overflow-y: auto; + width: 10em; + max-height: 11em; + } + + .mzta_autocomplete-item { + padding: 8px; + cursor: pointer; + } + + .mzta_autocomplete-item:hover { + background-color: #e9e9e9; + } + + .mzta_autocomplete-item-active { + background-color: #e9e9e9; + } + + + @media (prefers-color-scheme: dark) { + + body{ + background-color: #27272a; + } + + #mzta_popup{ + background-color: #27272a; + } + + input { + background-color: #27272a; + color: #ffffff; + } + + #mzta_search_banner{ + background-color: #27272a; + color:#ffffff; + } + + #mzta_autocomplete-items { + border: 1px solid #555; + background-color: #2c2c2c; + } + + .mzta_autocomplete-item { + color: #ffffff; + } + + .mzta_autocomplete-item:hover { + background-color: #444444; + } + + .mzta_autocomplete-item-active { + background-color: #444444; + } + } \ No newline at end of file diff --git a/popup/mzta-popup.html b/popup/mzta-popup.html new file mode 100644 index 00000000..a622bd2e --- /dev/null +++ b/popup/mzta-popup.html @@ -0,0 +1,20 @@ + + + + + + + ThunderAI Prompts + + + +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/popup/mzta-popup.js b/popup/mzta-popup.js new file mode 100644 index 00000000..5441d3be --- /dev/null +++ b/popup/mzta-popup.js @@ -0,0 +1,212 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import { taStore } from "../js/mzta-store.js"; + +document.addEventListener('DOMContentLoaded', async () => { + let tabId = await taStore.getSessionData("lastShortcutTabId"); + let tabType = await taStore.getSessionData("lastShortcutTabType"); + let filtering = await taStore.getSessionData("lastShortcutFiltering"); + let _prompts_data = await taStore.getSessionData("lastShortcutPromptsData"); + console.log(">>>>>>>>>>>>> _prompts_data: " + JSON.stringify(_prompts_data)); + let active_prompts = filterPromptsForTab(_prompts_data, filtering); + console.log(">>>>>>>>>>>>>> active_prompts: " + JSON.stringify(active_prompts)); + searchPrompt(active_prompts, tabId, tabType); + i18n.updateDocument(); +}, { once: true }); + +async function searchPrompt(allPrompts, tabId, tabType){ + console.log(">>>>>>>>>>>>>> allPrompts: " + JSON.stringify(allPrompts)); + console.log(">>>>>>>>>>>>>>> tabType: " + tabType); + + let input = document.getElementById('mzta_search_input'); + let autocompleteList = document.getElementById('mzta_autocomplete-items'); + let banner = document.getElementById('mzta_search_banner'); + + // Initialize variables to track focus and selection + let currentFocus = -1; // Tracks the currently highlighted item + let selectedId = null; // Tracks the ID of the selected item + + // Function to filter and display autocomplete suggestions + input.addEventListener('input', function() { + const query = this.value.trim().toLowerCase(); + autocompleteList.innerHTML = ''; // Clear previous suggestions + currentFocus = -1; // Reset the highlighted index + selectedId = null; // Reset the selected ID since input has changed + + // Uncomment the following lines if you want to hide suggestions when input is empty + /* + if (query === '') { + autocompleteList.style.display = 'none'; + return; + } + */ + + // Filter data based on the query + const filteredData = allPrompts.filter(item => + item.label.toLowerCase().startsWith(query) + ); + + if (filteredData.length === 0) { + autocompleteList.style.display = 'none'; + return; + } + + // Prepend numbers to the first 10 items + Array.from(filteredData).slice(0, 10).forEach((item, index) => { + const number = (index < 9) ? (index + 1).toString() : '0'; + // Check if the number is already prepended to avoid duplication + if (!item.numberPrepended) { + item.label = `${number}. ${item.label}`; + item.numberPrepended = 'true'; // Mark as prepended + } + }); + + // Create a div for each filtered result + filteredData.forEach(item => { + const itemDiv = document.createElement('div'); + itemDiv.classList.add('mzta_autocomplete-item'); + itemDiv.textContent = item.label; + itemDiv.setAttribute('data-id', item.id); + + // Add a mousedown event to select the item + itemDiv.addEventListener('mousedown', function(e) { // Use mousedown instead of click + e.preventDefault(); // Prevents the input from losing focus + input.value = item.label; + selectedId = item.id; // Store the selected item's ID + console.log('>>>>>>>>>>>>> mousedown selectedId:', selectedId); + autocompleteList.style.display = 'none'; + }); + + autocompleteList.appendChild(itemDiv); + }); + + autocompleteList.style.display = 'block'; + }); + + // Add a keydown event listener to handle arrow navigation and selection + input.addEventListener('keydown', function (e) { + + const items = autocompleteList.getElementsByClassName('mzta_autocomplete-item'); + if ((autocompleteList.style.display === 'none' || items.length === 0) + && (e.key !== 'Enter') + && !['1','2','3','4','5','6','7','8','9','0'].includes(e.key)) + { + return; // Do nothing if the autocomplete list is not visible + } + + // Handle number key presses (1-9,0) to select the corresponding item directly + if (['1','2','3','4','5','6','7','8','9','0'].includes(e.key)) { + // Map '1' to index 0, '2' to 1, ..., '9' to 8, '0' to 9 + const numIndex = (e.key === '0') ? 9 : parseInt(e.key, 10) - 1; + if (items[numIndex]) { + e.preventDefault(); // Prevent any default behavior + // Dispatch a mousedown event to simulate a click/select action + items[numIndex].dispatchEvent(new Event('mousedown')); + return; // Exit after handling the number key + } + } + + if (e.key === 'ArrowDown') { + // Navigate down the list + currentFocus++; + if (currentFocus >= items.length) currentFocus = 0; // Wrap to the first item + addActive(items); + e.preventDefault(); // Prevent cursor from moving to the end + } else if (e.key === 'ArrowUp') { + // Navigate up the list + currentFocus--; + if (currentFocus < 0) currentFocus = items.length - 1; // Wrap to the last item + addActive(items); + e.preventDefault(); // Prevent cursor from moving to the start + } else if (e.key === 'Enter') { + console.log(">>>>>>>>>>>>>> keydown == enter selectedId: " + selectedId); + if (selectedId) { + // If an item is already selected, call sendPrompt with the selected ID + e.preventDefault(); + sendPrompt(selectedId, tabId); // Call your sendPrompt function + banner.remove(); // Remove the banner after sending the prompt + } else { + // If no item is selected yet, select the highlighted item + // Select the highlighted item, or the first item if none is highlighted + e.preventDefault(); // Prevent form submission if inside a form + if (currentFocus > -1) { + if (items[currentFocus]) { + items[currentFocus].dispatchEvent(new Event('mousedown')); // Trigger the mousedown event + } + } else if (items.length > 0) { + // If no item is highlighted, select the first item + items[0].dispatchEvent(new Event('mousedown')); + } + } + } + }); + + // Function to add the "active" class to the current item + function addActive(items) { + removeActive(items); // Remove the "active" class from all items + if (currentFocus >= items.length) currentFocus = 0; + if (currentFocus < 0) currentFocus = items.length - 1; + items[currentFocus].classList.add('mzta_autocomplete-item-active'); // Add "active" class to the current item + // Ensure the active item is visible within the scrollable list + items[currentFocus].scrollIntoView({ + behavior: 'auto', // You can change to 'smooth' if you prefer smooth scrolling + block: 'nearest', // Align the item to the nearest edge of the visible area + }); + } + + // Function to remove the "active" class from all items + function removeActive(items) { + for (let i = 0; i < items.length; i++) { + items[i].classList.remove('mzta_autocomplete-item-active'); + } + } + +document.body.insertBefore(banner, document.body.firstChild); + setTimeout(() => { + input.dispatchEvent(new InputEvent('input', { bubbles: true })); + input.focus(); + }, 100); +} + + +function sendPrompt(prompt_id, tabId){ + console.log(">>>>>>>>>>>>> [ThunderAI] sendPrompt: " + prompt_id); + browser.runtime.sendMessage({command: "shortcut_do_prompt", tabId: tabId, promptId: prompt_id}); +} + +function filterPromptsForTab(prompts_data, filtering){ + // If filtering is 0, return the original array without any filters (btw it should not happen) + if (filtering === 0) { + return prompts_data; + } + + // Define the types to include based on the value of filtering + let allowedTypes; + if (filtering === 1) { + allowedTypes = ["0", "1"]; + } else if (filtering === 2) { + allowedTypes = ["0", "2"]; + } else { + // If filtering has an unexpected value, return the original data + return prompts_data; + } + + // Filter the array based on the allowed types + return prompts_data.filter(prompt => allowedTypes.includes(prompt.type)); +} \ No newline at end of file