parent
e6bc740adf
commit
cfc0aedb98
9 changed files with 334 additions and 12 deletions
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
BIN
images/custom_menu/empty_icon.png
Normal file
BIN
images/custom_menu/empty_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 705 B |
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
41
pages/menu_order/mzta-custom-menu-icons.js
Normal file
41
pages/menu_order/mzta-custom-menu-icons.js
Normal 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',
|
||||
];
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue