context menu icons added. see #680 #184

This commit is contained in:
Mic 2026-04-17 01:21:00 +02:00
parent e6bc740adf
commit cfc0aedb98
9 changed files with 334 additions and 12 deletions

View file

@ -2314,5 +2314,13 @@
"menu_order_hidden_items": {
"message": "Hidden items",
"description": "Section header for hidden menu items"
},
"menu_order_icon_label": {
"message": "Choose an icon",
"description": "Label/tooltip for the context menu icon picker on the menu order page"
},
"menu_order_icon_none": {
"message": "(none)",
"description": "Dropdown option meaning no custom icon is selected for the context menu item"
}
}

View file

@ -33,6 +33,7 @@ Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI
| `position_compose` | number | Sort order for the popup menu in compose view |
| `position_context` | number | Sort order for the context menu |
| `show_in` | string | `"popup"` = popup only, `"context"` = context menu only, `"both"` = both, `"none"` = hidden from all menus. Default: `"popup"` for default/custom prompts, `"both"` for special prompts |
| `custom_icon` | string | Filename (with extension) of an icon in `images/custom_menu/` used as the context-menu icon. Empty string = no icon. Only used for non-special prompts (special prompts use their hard-coded icons in `specialPromptToContextMenuID`). Selectable from a dropdown on the Menu Order page, context-menu tab. |
### Per-Prompt API Override Properties

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View file

@ -528,14 +528,18 @@ export class mzta_Menus {
// Create child menu items
for (const prompt of contextPrompts) {
const title = i18nConditionalGet(prompt.name);
const iconPath = getContextMenuIcon(prompt);
const menuOpts = {
id: 'mzta-ctx-' + prompt.id,
title: title,
contexts: ["message_list"],
parentId: 'mzta-context-parent',
};
if (iconPath) {
menuOpts.icons = iconPath;
}
await new Promise(resolve =>
browser.menus.create({
id: 'mzta-ctx-' + prompt.id,
title: title,
contexts: ["message_list"],
parentId: 'mzta-context-parent',
icons: getContextMenuIcon(prompt.id),
}, resolve)
browser.menus.create(menuOpts, resolve)
);
}
this.logger.log("Context menus loaded: " + contextPrompts.length + " items");

View file

@ -561,6 +561,7 @@ async function getDefaultPrompts_withProps() {
prompt.chatgpt_web_custom_gpt = (prefs._default_prompts_properties[prompt.id]?.chatgpt_web_custom_gpt || '').trim();
prompt.api_type = (prefs._default_prompts_properties[prompt.id]?.api_type || '').trim();
prompt.show_in = prefs._default_prompts_properties[prompt.id]?.show_in || prompt.show_in;
prompt.custom_icon = prefs._default_prompts_properties[prompt.id]?.custom_icon || "";
}else{
prompt.position_display = pos;
prompt.position_compose = pos;
@ -600,6 +601,9 @@ async function getCustomPrompts() {
if(prompt.show_in === undefined){
prompt.show_in = "popup";
}
if(prompt.custom_icon === undefined){
prompt.custom_icon = "";
}
});
return prefs._custom_prompt;
}
@ -619,6 +623,7 @@ export async function setDefaultPromptsProperties(prompts) {
chatgpt_web_custom_gpt: (prompt.chatgpt_web_custom_gpt === undefined || prompt.chatgpt_web_custom_gpt === "undefined") ? "" : prompt.chatgpt_web_custom_gpt,
api_type: (prompt.api_type === undefined || prompt.api_type === "undefined") ? "" : prompt.api_type,
show_in: (prompt.show_in === undefined || prompt.show_in === "undefined") ? "popup" : prompt.show_in,
custom_icon: (prompt.custom_icon === undefined || prompt.custom_icon === "undefined") ? "" : prompt.custom_icon,
};
});
//console.log('>>>>>>>>>>>>>> default_prompts_properties: ' + JSON.stringify(default_prompts_properties));

View file

@ -46,11 +46,22 @@ export const specialPromptToContextMenuID = {
// const defaultContextMenuIcon = 'moz-extension:images/icon-32px.png';
const defaultContextMenuIcon = '';
export function getContextMenuIcon(promptId) {
const contextMenuId = specialPromptToContextMenuID[promptId];
if (contextMenuId && contextMenuIconsPath[contextMenuId]) {
return contextMenuIconsPath[contextMenuId];
export function getContextMenuIcon(prompt) {
// Back-compat: accept a plain id string too
const promptId = (typeof prompt === 'string') ? prompt : prompt?.id;
const isSpecial = (typeof prompt === 'object' && prompt !== null) ? String(prompt.is_special) === '1' : true;
if (isSpecial) {
const contextMenuId = specialPromptToContextMenuID[promptId];
if (contextMenuId && contextMenuIconsPath[contextMenuId]) {
return contextMenuIconsPath[contextMenuId];
}
}
if (typeof prompt === 'object' && prompt !== null && prompt.custom_icon) {
return 'moz-extension:images/custom_menu/' + prompt.custom_icon;
}
return defaultContextMenuIcon;
}

View file

@ -0,0 +1,41 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 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 <http://www.gnu.org/licenses/>.
*/
export const customMenuIconsPath = 'images/custom_menu/';
export const customMenuIcons = [
'adventure-game.png',
'calendar.png',
'clapboard.png',
'clock.png',
'copywriting.png',
'customer-service.png',
'deadline.png',
'express-delivery.png',
'home.png',
'info.png',
'invoice.png',
'justice-scale.png',
'like.png',
'love-letter.png',
'policeman.png',
'printer.png',
'puzzle-game.png',
'scissors.png',
'send.png',
];

View file

@ -171,6 +171,86 @@
color: #e65100;
}
.item_icon_preview {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
cursor: pointer;
border: 1px solid transparent;
border-radius: 3px;
padding: 1px;
box-sizing: border-box;
}
.item_icon_preview:hover {
border-color: #409df3;
background: #eef5ff;
}
.item_icon_preview_empty {
display: inline-block;
background: transparent;
}
.item_icon_preview_special {
cursor: default;
}
.item_icon_preview_special:hover {
border-color: transparent;
background: transparent;
}
.icon_picker_popover {
position: absolute;
z-index: 1000;
background: #fff;
border: 1px solid #bbb;
border-radius: 6px;
padding: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
display: grid;
grid-template-columns: repeat(5, 28px);
gap: 4px;
max-width: 180px;
}
.icon_picker_cell {
width: 28px;
height: 28px;
padding: 2px;
border: 1px solid transparent;
background: transparent;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.icon_picker_cell img {
width: 20px;
height: 20px;
object-fit: contain;
}
.icon_picker_cell:hover {
background: #eef5ff;
border-color: #409df3;
}
.icon_picker_cell.selected {
background: #d4eaff;
border-color: #409df3;
}
.icon_picker_cell_none {
font-size: 1.2em;
color: #888;
}
/* Hidden list items */
.hidden_list .sortable_item {
opacity: 0.6;
@ -268,6 +348,29 @@
color: #ffb74d;
}
.item_icon_preview:hover {
background: #3a3a44;
border-color: #409df3;
}
.icon_picker_popover {
background: #2a2a30;
border-color: #555;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
}
.icon_picker_cell:hover {
background: #3a3a44;
}
.icon_picker_cell.selected {
background: #1a3a5c;
}
.icon_picker_cell_none {
color: #aaa;
}
.hidden_list .sortable_item {
background: #2a2a2e;
}

View file

@ -23,7 +23,17 @@ import {
setSpecialPrompts,
getHiddenSpecialPromptIds
} from '../../js/mzta-prompts.js';
import { i18nConditionalGet } from '../../js/mzta-utils.js';
import { i18nConditionalGet, specialPromptToContextMenuID, contextMenuIconsPath } from '../../js/mzta-utils.js';
import { customMenuIcons, customMenuIconsPath } from './mzta-custom-menu-icons.js';
// Convert "moz-extension:images/foo.png" to a relative path usable from this page
function resolveSpecialIconPath(promptId) {
const ctxId = specialPromptToContextMenuID[promptId];
if (!ctxId) return '';
const raw = contextMenuIconsPath[ctxId];
if (!raw) return '';
return '../../' + raw.replace(/^moz-extension:/, '');
}
let allPrompts = [];
let allExcludedSpecialPrompts = []; // special prompts excluded from UI (hidden + inactive features), preserved on save
@ -182,6 +192,15 @@ function renderListItems(listEl, items, menuType, isActive) {
handle.textContent = '\u2630';
li.appendChild(handle);
// Icon slot (context menu only) - between handle and toggle, to keep rows aligned
if (menuType === 'context') {
if (String(prompt.is_special) === '1') {
li.appendChild(buildSpecialIconDisplay(prompt));
} else {
li.appendChild(buildIconPicker(prompt));
}
}
// Toggle checkbox
const toggle = document.createElement('input');
toggle.type = 'checkbox';
@ -229,6 +248,136 @@ function renderListItems(listEl, items, menuType, isActive) {
});
}
// ==================== Custom icon picker ====================
let activeIconPopover = null;
function closeIconPopover() {
if (activeIconPopover) {
activeIconPopover.remove();
activeIconPopover = null;
document.removeEventListener('mousedown', onDocMouseDownForPopover, true);
document.removeEventListener('keydown', onDocKeyDownForPopover, true);
}
}
function onDocMouseDownForPopover(e) {
if (activeIconPopover && !activeIconPopover.contains(e.target) && !e.target.classList.contains('item_icon_preview')) {
closeIconPopover();
}
}
function onDocKeyDownForPopover(e) {
if (e.key === 'Escape') closeIconPopover();
}
function applyIconToPreview(preview, filename) {
if (filename) {
preview.src = '../../' + customMenuIconsPath + filename;
preview.classList.remove('item_icon_preview_empty');
} else {
preview.src = '../../' + customMenuIconsPath + 'empty_icon.png';
preview.classList.add('item_icon_preview_empty');
}
}
function buildSpecialIconDisplay(prompt) {
const img = document.createElement('img');
img.classList.add('item_icon_preview', 'item_icon_preview_special');
img.alt = '';
const path = resolveSpecialIconPath(prompt.id);
if (path) {
img.src = path;
} else {
img.src = '../../' + customMenuIconsPath + 'empty_icon.png';
img.classList.add('item_icon_preview_empty');
}
return img;
}
function buildIconPicker(prompt) {
const preview = document.createElement('img');
preview.classList.add('item_icon_preview');
preview.alt = '';
preview.title = browser.i18n.getMessage('menu_order_icon_label');
applyIconToPreview(preview, prompt.custom_icon || '');
preview.addEventListener('click', (e) => {
e.stopPropagation();
if (activeIconPopover && activeIconPopover.dataset.forId === prompt.id) {
closeIconPopover();
return;
}
closeIconPopover();
openIconPopover(preview, prompt);
});
return preview;
}
function openIconPopover(anchorEl, prompt) {
const popover = document.createElement('div');
popover.classList.add('icon_picker_popover');
popover.dataset.forId = prompt.id;
// "None" option
const noneBtn = document.createElement('button');
noneBtn.type = 'button';
noneBtn.classList.add('icon_picker_cell', 'icon_picker_cell_none');
noneBtn.title = browser.i18n.getMessage('menu_order_icon_none');
const noneImg = document.createElement('img');
noneImg.src = '../../' + customMenuIconsPath + 'empty_icon.png';
noneImg.alt = '';
noneBtn.appendChild(noneImg);
if (!prompt.custom_icon) noneBtn.classList.add('selected');
noneBtn.addEventListener('click', () => {
prompt.custom_icon = '';
applyIconToPreview(anchorEl, '');
markUnsaved();
closeIconPopover();
});
popover.appendChild(noneBtn);
customMenuIcons.forEach(filename => {
const btn = document.createElement('button');
btn.type = 'button';
btn.classList.add('icon_picker_cell');
btn.title = filename.replace(/\.[^.]+$/, '');
if (filename === prompt.custom_icon) btn.classList.add('selected');
const img = document.createElement('img');
img.src = '../../' + customMenuIconsPath + filename;
img.alt = '';
btn.appendChild(img);
btn.addEventListener('click', () => {
prompt.custom_icon = filename;
applyIconToPreview(anchorEl, filename);
markUnsaved();
closeIconPopover();
});
popover.appendChild(btn);
});
document.body.appendChild(popover);
activeIconPopover = popover;
// Position popover below the anchor
const rect = anchorEl.getBoundingClientRect();
const popRect = popover.getBoundingClientRect();
let left = rect.left + window.scrollX;
let top = rect.bottom + window.scrollY + 4;
if (left + popRect.width > window.scrollX + document.documentElement.clientWidth - 8) {
left = window.scrollX + document.documentElement.clientWidth - popRect.width - 8;
}
if (left < window.scrollX + 4) left = window.scrollX + 4;
popover.style.left = left + 'px';
popover.style.top = top + 'px';
document.addEventListener('mousedown', onDocMouseDownForPopover, true);
document.addEventListener('keydown', onDocKeyDownForPopover, true);
}
// ==================== Toggle show_in ====================
function toggleShowIn(prompt, menuType, isOn) {