diff --git a/_locales/en/messages.json b/_locales/en/messages.json
index 7e05c283..0c1df6e8 100644
--- a/_locales/en/messages.json
+++ b/_locales/en/messages.json
@@ -36,11 +36,11 @@
"description": ""
},
"prompt_summarize_this": {
- "message": "Summarize this",
+ "message": "Summarize",
"description": ""
},
"prompt_translate_this": {
- "message": "Translate this",
+ "message": "Translate",
"description": ""
},
"prompt_this": {
@@ -853,7 +853,7 @@
"description": ""
},
"prompt_add_tags": {
- "message": "Add tags to this email",
+ "message": "Add tags",
"description": ""
},
"prompt_add_tags_full_text": {
@@ -1393,7 +1393,7 @@
"description": ""
},
"prompt_spamfilter" : {
- "message" : "Detect spam emails",
+ "message" : "Analyze for spam",
"description" : ""
},
"prompt_spamfilter_full_text" : {
@@ -1500,22 +1500,6 @@
"message": "No",
"description": ""
},
- "context_menu_mzta-add-tags": {
- "message": "Add tags",
- "description": ""
- },
- "context_menu_mzta-spamfilter": {
- "message": "Analyze for spam",
- "description": ""
- },
- "context_menu_mzta-summarize": {
- "message": "Summarize",
- "description": ""
- },
- "context_menu_mzta-translate": {
- "message": "Translate",
- "description": ""
- },
"noActiveCalendar": {
"message": "No editable calendar found!",
"description": ""
@@ -2274,5 +2258,73 @@
"placeholder_string": {
"message": "Placeholder",
"description": ""
+ },
+ "menu_order_title": {
+ "message": "Menu Order",
+ "description": "Title for the menu order page"
+ },
+ "menu_order_popup_list_title": {
+ "message": "Popup Menu",
+ "description": "Header for the popup menu list in the menu order page"
+ },
+ "menu_order_context_list_title": {
+ "message": "Context Menu",
+ "description": "Header for the context menu list in the menu order page"
+ },
+ "menu_order_save": {
+ "message": "Save",
+ "description": "Save button label in menu order page"
+ },
+ "menu_order_saved": {
+ "message": "Menu order saved.",
+ "description": "Message shown after saving menu order"
+ },
+ "menu_order_tab_reading": {
+ "message": "Reading",
+ "description": "Sub-tab label for reading view ordering"
+ },
+ "menu_order_tab_composing": {
+ "message": "Composing",
+ "description": "Sub-tab label for composing view ordering"
+ },
+ "menu_order_badge_default": {
+ "message": "Default",
+ "description": "Badge label for default prompts"
+ },
+ "menu_order_badge_special": {
+ "message": "Special",
+ "description": "Badge label for special prompts"
+ },
+ "menu_order_badge_custom": {
+ "message": "Custom",
+ "description": "Badge label for custom prompts"
+ },
+ "menu_order_type_reading": {
+ "message": "Reading",
+ "description": "Badge label for reading-only prompts"
+ },
+ "menu_order_type_composing": {
+ "message": "Composing",
+ "description": "Badge label for composing-only prompts"
+ },
+ "menu_order_type_always": {
+ "message": "Always",
+ "description": "Badge label for prompts shown in all contexts"
+ },
+ "menu_order_btn_label": {
+ "message": "Menu Order",
+ "description": "Button label to open the menu order page from options"
+ },
+ "menu_order_info": {
+ "message": "Drag and drop items to reorder them. Use the toggle to show or hide items in each menu.",
+ "description": "Info text for the menu order page"
+ },
+ "menu_order_active_items": {
+ "message": "Visible items",
+ "description": "Section header for active/visible menu items"
+ },
+ "menu_order_hidden_items": {
+ "message": "Hidden items",
+ "description": "Section header for hidden menu items"
}
}
\ No newline at end of file
diff --git a/js/mzta-menus.js b/js/mzta-menus.js
index 1cea4993..b7230250 100644
--- a/js/mzta-menus.js
+++ b/js/mzta-menus.js
@@ -488,6 +488,7 @@ export class mzta_Menus {
is_special: prompt.is_special,
position_display: prompt.position_display,
position_compose: prompt.position_compose,
+ position_context: prompt.position_context,
};
this.shortcutMenu.push(curr_menu_entry);
}
@@ -514,8 +515,11 @@ export class mzta_Menus {
}, resolve)
);
- // Sort alphabetically for context menu
+ // Sort by position_context if available, otherwise alphabetically
contextPrompts.sort((a, b) => {
+ const posA = a.position_context || 9999;
+ const posB = b.position_context || 9999;
+ if (posA !== posB) return posA - posB;
const nameA = i18nConditionalGet(a.name);
const nameB = i18nConditionalGet(b.name);
return nameA.localeCompare(nameB);
diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js
index 089f7184..3c924af3 100644
--- a/js/mzta-prompts.js
+++ b/js/mzta-prompts.js
@@ -383,7 +383,7 @@ const specialPrompts = [
api_model: '',
is_default: "1",
is_special: "1",
- show_in: "both",
+ show_in: "none",
},
{
id: 'prompt_summarize_email_separator',
@@ -400,7 +400,7 @@ const specialPrompts = [
api_model: '',
is_default: "1",
is_special: "1",
- show_in: "both",
+ show_in: "none",
},
{
id: 'prompt_translate_this',
@@ -488,7 +488,7 @@ export function preparePromptsForExport(prompts, include_api_settings = false){
}
if(prompt.is_default == 1){
- let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'need_custom_text', 'show_in'];
+ let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'position_context', 'need_custom_text', 'show_in'];
if(include_api_settings){
allowedKeys.push('api_type');
for (const [integration, options] of Object.entries(integration_options_config)) {
@@ -541,6 +541,7 @@ async function getDefaultPrompts_withProps() {
prompt.text = browser.i18n.getMessage(prompt.text);
prompt.position_display = pos;
prompt.position_compose = pos;
+ prompt.position_context = pos;
prompt.enabled = 1;
pos++;
})
@@ -552,6 +553,7 @@ async function getDefaultPrompts_withProps() {
if(prefs._default_prompts_properties?.[prompt.id]){
prompt.position_compose = prefs._default_prompts_properties[prompt.id].position_compose;
prompt.position_display = prefs._default_prompts_properties[prompt.id].position_display;
+ prompt.position_context = prefs._default_prompts_properties[prompt.id]?.position_context || prompt.position_display;
prompt.enabled = prefs._default_prompts_properties[prompt.id].enabled;
prompt.need_custom_text = prefs._default_prompts_properties[prompt.id].need_custom_text;
prompt.chatgpt_web_model = prefs._default_prompts_properties[prompt.id].chatgpt_web_model;
@@ -562,6 +564,7 @@ async function getDefaultPrompts_withProps() {
}else{
prompt.position_display = pos;
prompt.position_compose = pos;
+ prompt.position_context = pos;
prompt.enabled = 1;
pos++;
}
@@ -608,6 +611,7 @@ export async function setDefaultPromptsProperties(prompts) {
default_prompts_properties[prompt.id] = {
position_compose: (prompt.position_compose === undefined || prompt.position_compose === "undefined") ? "" : prompt.position_compose,
position_display: (prompt.position_display === undefined || prompt.position_display === "undefined") ? "" : prompt.position_display,
+ position_context: (prompt.position_context === undefined || prompt.position_context === "undefined") ? "" : prompt.position_context,
enabled: (prompt.enabled === undefined || prompt.enabled === "undefined") ? "" : prompt.enabled,
need_custom_text: (prompt.need_custom_text === undefined || prompt.need_custom_text === "undefined") ? "" : prompt.need_custom_text,
chatgpt_web_model: (prompt.chatgpt_web_model === undefined || prompt.chatgpt_web_model === "undefined") ? "" : prompt.chatgpt_web_model,
@@ -666,6 +670,10 @@ export async function setSpecialPrompts(prompts) {
await browser.storage.local.set({_special_prompts: prompts});
}
+export function getHiddenSpecialPromptIds() {
+ return specialPrompts.filter(p => p.show_in === "none").map(p => p.id);
+}
+
export async function getSpamFilterPrompt(){
return (await getSpecialPrompts()).find(prompt => prompt.id == 'prompt_spamfilter');
}
diff --git a/options/mzta-options.html b/options/mzta-options.html
index ba3391c7..1f3d6eb4 100644
--- a/options/mzta-options.html
+++ b/options/mzta-options.html
@@ -271,6 +271,7 @@
__MSG_prefs_OptionText_btnManagePrompts_infoline__ __MSG_more_info_string__
+
__MSG_prefsInfoTitle__
diff --git a/options/mzta-options.js b/options/mzta-options.js
index 0567e870..5f34710f 100644
--- a/options/mzta-options.js
+++ b/options/mzta-options.js
@@ -412,6 +412,10 @@ document.addEventListener('DOMContentLoaded', async () => {
openTab('/pages/customdataplaceholders/mzta-custom-dataplaceholders.html');
});
+ document.getElementById('btnMenuOrder').addEventListener('click', () => {
+ openTab('/pages/menu_order/mzta-menu-order.html');
+ });
+
document.getElementById('btnManageTagsInfo').addEventListener('click', () => {
openTab('/pages/addtags/mzta-add-tags.html');
});
diff --git a/pages/menu_order/mzta-menu-order.css b/pages/menu_order/mzta-menu-order.css
new file mode 100644
index 00000000..1aacf6f4
--- /dev/null
+++ b/pages/menu_order/mzta-menu-order.css
@@ -0,0 +1,291 @@
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ margin: 20px;
+ padding: 0;
+}
+
+.page_title {
+ margin-bottom: 4px;
+}
+
+.info_text {
+ margin-top: 0;
+ font-size: 0.9em;
+ color: #555;
+}
+
+#command_palette {
+ margin-bottom: 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+#btnSave {
+ padding: 6px 20px;
+ font-size: 1em;
+ cursor: pointer;
+}
+
+#btnSave:disabled {
+ opacity: 0.5;
+ cursor: default;
+}
+
+#msgDisplay {
+ font-size: 0.9em;
+ color: #2a7e2a;
+}
+
+#lists_container {
+ display: flex;
+ gap: 24px;
+ align-items: flex-start;
+}
+
+.menu_list_panel {
+ flex: 1;
+ min-width: 320px;
+ border: 1px solid #ccc;
+ border-radius: 6px;
+ padding: 12px;
+}
+
+.menu_list_panel h2 {
+ margin-top: 0;
+ margin-bottom: 8px;
+ font-size: 1.1em;
+}
+
+.sub_tabs {
+ display: flex;
+ gap: 4px;
+ margin-bottom: 12px;
+}
+
+.sub_tab {
+ padding: 4px 14px;
+ border: 1px solid #ccc;
+ background: #f5f5f5;
+ cursor: pointer;
+ border-radius: 4px 4px 0 0;
+ font-size: 0.9em;
+}
+
+.sub_tab.active {
+ background: #fff;
+ border-bottom-color: #fff;
+ font-weight: bold;
+}
+
+.section_label {
+ font-size: 0.85em;
+ color: #666;
+ margin: 8px 0 4px 0;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.hidden_section_label {
+ border-top: 1px dashed #ccc;
+ padding-top: 8px;
+ margin-top: 12px;
+}
+
+.sortable_list,
+.hidden_list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ min-height: 40px;
+}
+
+.sortable_item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 8px;
+ margin: 3px 0;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ background: #fff;
+ user-select: none;
+ transition: background-color 0.15s;
+}
+
+.sortable_item:hover {
+ background: #f0f4ff;
+}
+
+.sortable_item.dragging {
+ opacity: 0.4;
+ border-style: dashed;
+}
+
+.sortable_item.drag-over {
+ border-top: 2px solid #409df3;
+}
+
+.drag_handle {
+ cursor: grab;
+ color: #999;
+ font-size: 1.1em;
+ padding: 0 4px;
+ flex-shrink: 0;
+}
+
+.drag_handle:active {
+ cursor: grabbing;
+}
+
+.item_toggle {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+ cursor: pointer;
+}
+
+.item_name {
+ flex: 1;
+ font-size: 0.92em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.badge {
+ display: inline-block;
+ padding: 1px 6px;
+ border-radius: 3px;
+ font-size: 0.72em;
+ font-weight: 600;
+ text-transform: uppercase;
+ flex-shrink: 0;
+}
+
+.badge_default {
+ background: #e0e0e0;
+ color: #555;
+}
+
+.badge_special {
+ background: #d4eaff;
+ color: #1a5fa0;
+}
+
+.badge_custom {
+ background: #e8f5e9;
+ color: #2e7d32;
+}
+
+.badge_type {
+ background: #fff3e0;
+ color: #e65100;
+}
+
+/* Hidden list items */
+.hidden_list .sortable_item {
+ opacity: 0.6;
+ background: #fafafa;
+}
+
+.hidden_list .sortable_item .drag_handle {
+ visibility: hidden;
+}
+
+.hidden_list .sortable_item:hover {
+ opacity: 0.8;
+ background: #f5f5f5;
+}
+
+/* Dark mode */
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #1c1b22;
+ color: rgb(251, 251, 254);
+ }
+
+ .info_text {
+ color: #aaa;
+ }
+
+ #msgDisplay {
+ color: #5cb85c;
+ }
+
+ .menu_list_panel {
+ border-color: #444;
+ background: #2a2a30;
+ }
+
+ .sub_tab {
+ border-color: #555;
+ background: #333;
+ color: #ccc;
+ }
+
+ .sub_tab.active {
+ background: #2a2a30;
+ border-bottom-color: #2a2a30;
+ color: #fff;
+ }
+
+ .section_label {
+ color: #999;
+ }
+
+ .hidden_section_label {
+ border-top-color: #555;
+ }
+
+ .sortable_item {
+ border-color: #444;
+ background: #333;
+ color: #eee;
+ }
+
+ .sortable_item:hover {
+ background: #3a3a44;
+ }
+
+ .sortable_item.drag-over {
+ border-top-color: #409df3;
+ }
+
+ .drag_handle {
+ color: #777;
+ }
+
+ .badge_default {
+ background: #444;
+ color: #bbb;
+ }
+
+ .badge_special {
+ background: #1a3a5c;
+ color: #7ab8f5;
+ }
+
+ .badge_custom {
+ background: #1b3a1e;
+ color: #81c784;
+ }
+
+ .badge_type {
+ background: #3d2800;
+ color: #ffb74d;
+ }
+
+ .hidden_list .sortable_item {
+ background: #2a2a2e;
+ }
+
+ .hidden_list .sortable_item:hover {
+ background: #333;
+ }
+
+ a:link { color: #409eff; }
+ a:visited { color: #409eff; }
+ a:hover { color: #66b1ff; }
+ a:active { color: #66b1ff; }
+}
diff --git a/pages/menu_order/mzta-menu-order.html b/pages/menu_order/mzta-menu-order.html
new file mode 100644
index 00000000..ad5fe728
--- /dev/null
+++ b/pages/menu_order/mzta-menu-order.html
@@ -0,0 +1,39 @@
+
+
+
+
+
ThunderAI - __MSG_menu_order_title__
+
+
+
+
+
__MSG_menu_order_title__
+
__MSG_menu_order_info__
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/menu_order/mzta-menu-order.js b/pages/menu_order/mzta-menu-order.js
new file mode 100644
index 00000000..3335fa0e
--- /dev/null
+++ b/pages/menu_order/mzta-menu-order.js
@@ -0,0 +1,317 @@
+/*
+ * 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
.
+ */
+
+import { getPrompts, setDefaultPromptsProperties, setCustomPrompts, setSpecialPrompts, getHiddenSpecialPromptIds } from '../../js/mzta-prompts.js';
+import { i18nConditionalGet } from '../../js/mzta-utils.js';
+
+let allPrompts = [];
+let currentPopupView = 'display'; // 'display' or 'compose'
+let hasUnsavedChanges = false;
+
+document.addEventListener('DOMContentLoaded', async () => {
+ allPrompts = await getPrompts(false, [], true);
+
+ // Exclude special prompts that are defined with show_in: "none" (internal prompts, not user-toggleable)
+ const hiddenSpecialIds = getHiddenSpecialPromptIds();
+ allPrompts = allPrompts.filter(p => !hiddenSpecialIds.includes(p.id));
+
+ // Resolve i18n names and assign initial position_context if missing
+ let contextPos = 1;
+ const sortedForContext = [...allPrompts].sort((a, b) => {
+ const nameA = i18nConditionalGet(a.name);
+ const nameB = i18nConditionalGet(b.name);
+ return nameA.localeCompare(nameB);
+ });
+ sortedForContext.forEach(p => {
+ if (p.position_context === undefined || p.position_context === '' || p.position_context === 'undefined') {
+ p.position_context = contextPos;
+ }
+ contextPos++;
+ });
+
+ // Resolve display names
+ allPrompts.forEach(p => {
+ p._displayName = i18nConditionalGet(p.name);
+ });
+
+ renderPopupList();
+ renderContextList();
+ initSubTabs();
+
+ document.getElementById('btnSave').addEventListener('click', saveAll);
+
+ i18n.updateDocument();
+});
+
+// ==================== Sub-tabs ====================
+
+function initSubTabs() {
+ document.querySelectorAll('.sub_tab').forEach(btn => {
+ btn.addEventListener('click', () => {
+ document.querySelectorAll('.sub_tab').forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+ currentPopupView = btn.dataset.view;
+ renderPopupList();
+ });
+ });
+}
+
+// ==================== Render Popup List ====================
+
+function renderPopupList() {
+ const posKey = currentPopupView === 'display' ? 'position_display' : 'position_compose';
+
+ const activeItems = allPrompts.filter(p => {
+ const showIn = p.show_in || 'popup';
+ return showIn === 'popup' || showIn === 'both';
+ });
+ const hiddenItems = allPrompts.filter(p => {
+ const showIn = p.show_in || 'popup';
+ return showIn !== 'popup' && showIn !== 'both';
+ });
+
+ activeItems.sort((a, b) => (a[posKey] || 9999) - (b[posKey] || 9999));
+ hiddenItems.sort((a, b) => a._displayName.localeCompare(b._displayName));
+
+ const activeList = document.getElementById('popup_list');
+ const hiddenList = document.getElementById('popup_list_hidden');
+
+ renderListItems(activeList, activeItems, 'popup', true);
+ renderListItems(hiddenList, hiddenItems, 'popup', false);
+
+ initDragAndDrop(activeList, posKey);
+}
+
+// ==================== Render Context List ====================
+
+function renderContextList() {
+ // Only show items with type 0 or 1 (not composing-only)
+ const contextEligible = allPrompts.filter(p => String(p.type) === '0' || String(p.type) === '1');
+
+ const activeItems = contextEligible.filter(p => {
+ const showIn = p.show_in || 'popup';
+ return showIn === 'context' || showIn === 'both';
+ });
+ const hiddenItems = contextEligible.filter(p => {
+ const showIn = p.show_in || 'popup';
+ return showIn !== 'context' && showIn !== 'both';
+ });
+
+ activeItems.sort((a, b) => (a.position_context || 9999) - (b.position_context || 9999));
+ hiddenItems.sort((a, b) => a._displayName.localeCompare(b._displayName));
+
+ const activeList = document.getElementById('context_list');
+ const hiddenList = document.getElementById('context_list_hidden');
+
+ renderListItems(activeList, activeItems, 'context', true);
+ renderListItems(hiddenList, hiddenItems, 'context', false);
+
+ initDragAndDrop(activeList, 'position_context');
+}
+
+// ==================== Render List Items ====================
+
+function renderListItems(listEl, items, menuType, isActive) {
+ listEl.innerHTML = '';
+ items.forEach(prompt => {
+ const li = document.createElement('li');
+ li.classList.add('sortable_item');
+ li.dataset.id = prompt.id;
+ if (isActive) {
+ li.draggable = true;
+ }
+
+ // Drag handle
+ const handle = document.createElement('span');
+ handle.classList.add('drag_handle');
+ handle.textContent = '\u2630';
+ li.appendChild(handle);
+
+ // Toggle checkbox
+ const toggle = document.createElement('input');
+ toggle.type = 'checkbox';
+ toggle.classList.add('item_toggle');
+ toggle.checked = isActive;
+ toggle.addEventListener('change', () => {
+ toggleShowIn(prompt, menuType, toggle.checked);
+ });
+ li.appendChild(toggle);
+
+ // Name
+ const nameSpan = document.createElement('span');
+ nameSpan.classList.add('item_name');
+ nameSpan.textContent = prompt._displayName;
+ li.appendChild(nameSpan);
+
+ // Type badge
+ const typeBadge = document.createElement('span');
+ typeBadge.classList.add('badge', 'badge_type');
+ if (String(prompt.type) === '1') {
+ typeBadge.textContent = browser.i18n.getMessage('menu_order_type_reading');
+ } else if (String(prompt.type) === '2') {
+ typeBadge.textContent = browser.i18n.getMessage('menu_order_type_composing');
+ } else {
+ typeBadge.textContent = browser.i18n.getMessage('menu_order_type_always');
+ }
+ li.appendChild(typeBadge);
+
+ // Source badge
+ const sourceBadge = document.createElement('span');
+ sourceBadge.classList.add('badge');
+ if (String(prompt.is_special) === '1') {
+ sourceBadge.classList.add('badge_special');
+ sourceBadge.textContent = browser.i18n.getMessage('menu_order_badge_special');
+ } else if (String(prompt.is_default) === '1') {
+ sourceBadge.classList.add('badge_default');
+ sourceBadge.textContent = browser.i18n.getMessage('menu_order_badge_default');
+ } else {
+ sourceBadge.classList.add('badge_custom');
+ sourceBadge.textContent = browser.i18n.getMessage('menu_order_badge_custom');
+ }
+ li.appendChild(sourceBadge);
+
+ listEl.appendChild(li);
+ });
+}
+
+// ==================== Toggle show_in ====================
+
+function toggleShowIn(prompt, menuType, isOn) {
+ const current = prompt.show_in || 'popup';
+
+ if (menuType === 'popup') {
+ if (isOn) {
+ prompt.show_in = (current === 'none') ? 'popup' : (current === 'context') ? 'both' : current;
+ } else {
+ prompt.show_in = (current === 'popup') ? 'none' : (current === 'both') ? 'context' : current;
+ }
+ } else { // context
+ if (isOn) {
+ prompt.show_in = (current === 'none') ? 'context' : (current === 'popup') ? 'both' : current;
+ } else {
+ prompt.show_in = (current === 'context') ? 'none' : (current === 'both') ? 'popup' : current;
+ }
+ }
+
+ markUnsaved();
+ renderPopupList();
+ renderContextList();
+}
+
+// ==================== Drag and Drop ====================
+
+function initDragAndDrop(listEl, positionKey) {
+ let draggedItem = null;
+
+ listEl.addEventListener('dragstart', (e) => {
+ const li = e.target.closest('.sortable_item');
+ if (!li) return;
+ draggedItem = li;
+ draggedItem.classList.add('dragging');
+ e.dataTransfer.effectAllowed = 'move';
+ e.dataTransfer.setData('text/plain', li.dataset.id);
+ });
+
+ listEl.addEventListener('dragover', (e) => {
+ e.preventDefault();
+ e.dataTransfer.dropEffect = 'move';
+ if (!draggedItem) return;
+
+ // Remove previous drag-over indicators
+ listEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+
+ const afterElement = getDragAfterElement(listEl, e.clientY);
+ if (afterElement) {
+ afterElement.classList.add('drag-over');
+ listEl.insertBefore(draggedItem, afterElement);
+ } else {
+ listEl.appendChild(draggedItem);
+ }
+ });
+
+ listEl.addEventListener('dragleave', (e) => {
+ if (e.target.classList) {
+ e.target.classList.remove('drag-over');
+ }
+ });
+
+ listEl.addEventListener('drop', (e) => {
+ e.preventDefault();
+ listEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
+ });
+
+ listEl.addEventListener('dragend', () => {
+ if (draggedItem) {
+ draggedItem.classList.remove('dragging');
+ updatePositionsFromDOM(listEl, positionKey);
+ draggedItem = null;
+ markUnsaved();
+ }
+ });
+}
+
+function getDragAfterElement(container, y) {
+ const draggableElements = [...container.querySelectorAll('.sortable_item:not(.dragging)')];
+ return draggableElements.reduce((closest, child) => {
+ const box = child.getBoundingClientRect();
+ const offset = y - box.top - box.height / 2;
+ if (offset < 0 && offset > closest.offset) {
+ return { offset: offset, element: child };
+ }
+ return closest;
+ }, { offset: Number.NEGATIVE_INFINITY }).element;
+}
+
+function updatePositionsFromDOM(listEl, positionKey) {
+ const items = listEl.querySelectorAll('.sortable_item');
+ items.forEach((li, index) => {
+ const promptId = li.dataset.id;
+ const prompt = allPrompts.find(p => p.id === promptId);
+ if (prompt) {
+ prompt[positionKey] = index + 1;
+ }
+ });
+}
+
+// ==================== Save ====================
+
+async function saveAll() {
+ const btnSave = document.getElementById('btnSave');
+ const msgDisplay = document.getElementById('msgDisplay');
+ btnSave.disabled = true;
+
+ const defaultPromptsToSave = allPrompts.filter(p => String(p.is_default) === '1' && String(p.is_special) !== '1');
+ const customPromptsToSave = allPrompts.filter(p => String(p.is_default) === '0' && String(p.is_special) !== '1');
+ const specialPromptsToSave = allPrompts.filter(p => String(p.is_special) === '1');
+
+ await setDefaultPromptsProperties(defaultPromptsToSave);
+ await setCustomPrompts(customPromptsToSave);
+ await setSpecialPrompts(specialPromptsToSave);
+
+ await browser.runtime.sendMessage({ command: "reload_menus" });
+
+ hasUnsavedChanges = false;
+ msgDisplay.textContent = browser.i18n.getMessage('menu_order_saved');
+ setTimeout(() => { msgDisplay.textContent = ''; }, 3000);
+}
+
+function markUnsaved() {
+ hasUnsavedChanges = true;
+ document.getElementById('btnSave').disabled = false;
+ document.getElementById('msgDisplay').textContent = '';
+}