Skip to content

Commit 8c411f0

Browse files
authored
Merge pull request #28 from JOHLC/copilot/add-multiple-context-menu-options
[WIP] Add ability to use multiple right-click context menu options
2 parents 2452fd5 + e678df6 commit 8c411f0

File tree

5 files changed

+273
-18
lines changed

5 files changed

+273
-18
lines changed

package/background.js

Lines changed: 59 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,47 @@ importScripts('utils.js');
1717
* Initialize context menus when extension is installed or updated
1818
*/
1919
chrome.runtime.onInstalled.addListener(() => {
20+
createContextMenus();
21+
});
22+
23+
/**
24+
* Create context menus from stored configuration
25+
*/
26+
function createContextMenus() {
2027
chrome.contextMenus.removeAll(() => {
21-
// Parent menu
22-
chrome.contextMenus.create({
23-
id: 'send-to-ha-parent',
24-
title: 'Send to Home Assistant',
25-
contexts: ['page', 'selection', 'link'],
26-
});
27-
// Default sub-option
28-
chrome.contextMenus.create({
29-
id: 'send-to-ha-default',
30-
parentId: 'send-to-ha-parent',
31-
title: 'Default',
32-
contexts: ['page', 'selection', 'link'],
28+
// Get stored context menu items
29+
chrome.storage.sync.get(['contextMenuItems'], (result) => {
30+
let menuItems = [{ id: 'default', name: 'Default' }]; // Default fallback
31+
32+
if (result.contextMenuItems && Array.isArray(result.contextMenuItems)) {
33+
menuItems = result.contextMenuItems;
34+
}
35+
36+
// Parent menu
37+
chrome.contextMenus.create({
38+
id: 'send-to-ha-parent',
39+
title: 'Send to Home Assistant',
40+
contexts: ['page', 'selection', 'link'],
41+
});
42+
43+
// Create sub-menu for each item
44+
menuItems.forEach((item) => {
45+
chrome.contextMenus.create({
46+
id: `send-to-ha-${item.id}`,
47+
parentId: 'send-to-ha-parent',
48+
title: item.name,
49+
contexts: ['page', 'selection', 'link'],
50+
});
51+
});
3352
});
34-
// Future sub-options can be added here
3553
});
54+
}
55+
56+
// Also recreate menus when storage changes (options page updates)
57+
chrome.storage.onChanged.addListener((changes, areaName) => {
58+
if (areaName === 'sync' && changes.contextMenuItems) {
59+
createContextMenus();
60+
}
3661
});
3762

3863
/**
@@ -50,21 +75,38 @@ chrome.contextMenus.onClicked.addListener(async(info, tab) => {
5075
return;
5176
}
5277

53-
if (info.menuItemId === 'send-to-ha-default') {
54-
await handleContextMenuSend(info, tab);
78+
// Extract context from menu item ID (e.g., 'send-to-ha-default' -> 'default')
79+
const menuItemId = info.menuItemId;
80+
if (typeof menuItemId === 'string' && menuItemId.startsWith('send-to-ha-')) {
81+
const contextId = menuItemId.replace('send-to-ha-', '');
82+
83+
// Get the menu item name from storage
84+
chrome.storage.sync.get(['contextMenuItems'], async(result) => {
85+
let contextName = contextId; // Default to ID if not found
86+
87+
if (result.contextMenuItems && Array.isArray(result.contextMenuItems)) {
88+
const menuItem = result.contextMenuItems.find(item => item.id === contextId);
89+
if (menuItem) {
90+
contextName = menuItem.name;
91+
}
92+
}
93+
94+
await handleContextMenuSend(info, tab, contextName);
95+
});
5596
}
56-
// Future sub-options can be handled here
5797
});
5898

5999
/**
60100
* Handle sending from context menu
61101
* @param {object} info - Context menu info
62102
* @param {object} tab - Tab information
103+
* @param {string} contextName - Name of the context menu item used
63104
*/
64-
async function handleContextMenuSend(info, tab) {
105+
async function handleContextMenuSend(info, tab, contextName) {
65106
await ExtensionUtils.sendToHomeAssistant({
66107
tab,
67108
contextInfo: info,
109+
context: contextName,
68110
});
69111
}
70112

package/options.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,16 @@ <h2>Send to Home Assistant</h2>
3030
<label for="deviceName">Device Name <span class="label-note">(optional, for identifying this device)</span></label>
3131
<input type="text" id="deviceName" placeholder="e.g. Work Laptop" maxlength="32">
3232

33+
<span class="options-title">Context Menu Options</span>
34+
<p>Add custom context menu items to route different actions in Home Assistant. Each menu item will include a "context" field in the JSON payload.</p>
35+
36+
<div id="contextMenuItems"></div>
37+
38+
<div class="add-menu-item-row">
39+
<input type="text" id="newMenuItemName" placeholder="Enter menu item name (e.g., Automation)" maxlength="32">
40+
<button id="addMenuItem" class="copy-btn">Add</button>
41+
</div>
42+
3343
<div class="btn-row">
3444
<button id="test" class="copy-btn">Test</button>
3545
<button id="save" class="copy-btn">Save</button>

package/options.js

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ const statusDiv = document.getElementById('status');
2121
const saveBtn = document.getElementById('save');
2222
const testBtn = document.getElementById('test');
2323
const clearBtn = document.getElementById('clearConfig');
24+
const newMenuItemInput = document.getElementById('newMenuItemName');
25+
const addMenuItemBtn = document.getElementById('addMenuItem');
26+
const contextMenuItemsDiv = document.getElementById('contextMenuItems');
27+
28+
// --- Context Menu Items Management ---
29+
30+
let contextMenuItems = [{ id: 'default', name: 'Default' }]; // Default item always present
2431

2532
// --- Initialization ---
2633

@@ -33,6 +40,7 @@ document.addEventListener('DOMContentLoaded', function() {
3340
loadSavedConfiguration();
3441
setupEventListeners();
3542
updateSslWarning();
43+
loadContextMenuItems();
3644
});
3745

3846
/**
@@ -143,6 +151,116 @@ function loadSavedConfiguration() {
143151
});
144152
}
145153

154+
/**
155+
* Load context menu items from storage
156+
*/
157+
function loadContextMenuItems() {
158+
chrome.storage.sync.get(['contextMenuItems'], (result) => {
159+
if (result.contextMenuItems && Array.isArray(result.contextMenuItems)) {
160+
contextMenuItems = result.contextMenuItems;
161+
}
162+
renderContextMenuItems();
163+
});
164+
}
165+
166+
/**
167+
* Render context menu items in the UI
168+
*/
169+
function renderContextMenuItems() {
170+
contextMenuItemsDiv.innerHTML = '';
171+
172+
contextMenuItems.forEach((item, index) => {
173+
const itemDiv = document.createElement('div');
174+
itemDiv.className = 'menu-item';
175+
176+
const nameSpan = document.createElement('span');
177+
nameSpan.className = 'menu-item-name';
178+
nameSpan.textContent = item.name;
179+
180+
// Add "Default" indicator for the default item
181+
if (item.id === 'default') {
182+
const defaultSpan = document.createElement('span');
183+
defaultSpan.className = 'menu-item-default';
184+
defaultSpan.textContent = '(Default)';
185+
nameSpan.appendChild(defaultSpan);
186+
}
187+
188+
itemDiv.appendChild(nameSpan);
189+
190+
// Only allow removing non-default items
191+
if (item.id !== 'default') {
192+
const removeBtn = document.createElement('button');
193+
removeBtn.className = 'menu-item-remove';
194+
removeBtn.textContent = 'Remove';
195+
removeBtn.addEventListener('click', () => removeContextMenuItem(index));
196+
itemDiv.appendChild(removeBtn);
197+
}
198+
199+
contextMenuItemsDiv.appendChild(itemDiv);
200+
});
201+
}
202+
203+
/**
204+
* Add a new context menu item
205+
*/
206+
function addContextMenuItem() {
207+
const name = newMenuItemInput.value.trim();
208+
209+
if (!name) {
210+
showStatus('Please enter a menu item name.', 'error');
211+
return;
212+
}
213+
214+
if (name.length > 32) {
215+
showStatus('Menu item name cannot exceed 32 characters.', 'error');
216+
return;
217+
}
218+
219+
// Check for duplicate names
220+
if (contextMenuItems.some(item => item.name.toLowerCase() === name.toLowerCase())) {
221+
showStatus('A menu item with this name already exists.', 'error');
222+
return;
223+
}
224+
225+
// Generate unique ID
226+
const id = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
227+
228+
contextMenuItems.push({ id, name });
229+
230+
// Save to storage
231+
chrome.storage.sync.set({ contextMenuItems }, () => {
232+
renderContextMenuItems();
233+
newMenuItemInput.value = '';
234+
showStatus('Menu item added! Click "Save" to apply changes.', 'success');
235+
setTimeout(clearStatus, 2000);
236+
});
237+
}
238+
239+
/**
240+
* Remove a context menu item
241+
* @param {number} index - Index of the item to remove
242+
*/
243+
function removeContextMenuItem(index) {
244+
if (index < 0 || index >= contextMenuItems.length) {
245+
return;
246+
}
247+
248+
// Don't allow removing the default item
249+
if (contextMenuItems[index].id === 'default') {
250+
showStatus('Cannot remove the default menu item.', 'error');
251+
return;
252+
}
253+
254+
contextMenuItems.splice(index, 1);
255+
256+
// Save to storage
257+
chrome.storage.sync.set({ contextMenuItems }, () => {
258+
renderContextMenuItems();
259+
showStatus('Menu item removed! Click "Save" to apply changes.', 'success');
260+
setTimeout(clearStatus, 2000);
261+
});
262+
}
263+
146264
/**
147265
* Setup event listeners for various UI elements
148266
*/
@@ -160,6 +278,20 @@ function setupEventListeners() {
160278
if (clearBtn) {
161279
clearBtn.addEventListener('click', handleClearConfig);
162280
}
281+
282+
// Add menu item button handler
283+
if (addMenuItemBtn) {
284+
addMenuItemBtn.addEventListener('click', addContextMenuItem);
285+
}
286+
287+
// Allow adding menu item by pressing Enter
288+
if (newMenuItemInput) {
289+
newMenuItemInput.addEventListener('keypress', (e) => {
290+
if (e.key === 'Enter') {
291+
addContextMenuItem();
292+
}
293+
});
294+
}
163295
}
164296

165297
// --- Configuration Management ---
@@ -226,7 +358,7 @@ function handleTest() {
226358
* Handle clear config button click
227359
*/
228360
function handleClearConfig() {
229-
chrome.storage.sync.remove(['haHost', 'ssl', 'webhookId', 'userName', 'deviceName'], () => {
361+
chrome.storage.sync.remove(['haHost', 'ssl', 'webhookId', 'userName', 'deviceName', 'contextMenuItems'], () => {
230362
// Clear form fields
231363
hostInput.value = '';
232364
sslToggle.checked = true;
@@ -236,6 +368,10 @@ function handleClearConfig() {
236368
deviceInput.value = '';
237369
}
238370

371+
// Reset context menu items to default
372+
contextMenuItems = [{ id: 'default', name: 'Default' }];
373+
renderContextMenuItems();
374+
239375
showStatus('Config cleared!', 'success');
240376
setTimeout(clearStatus, 2000);
241377
});

package/style.css

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,3 +381,63 @@ button:active, .ok-btn:active {
381381
outline: none;
382382
box-shadow: 0 4px 16px #00b4fc55;
383383
}
384+
385+
/* Context Menu Items Management */
386+
.add-menu-item-row {
387+
display: flex;
388+
gap: 0.5em;
389+
width: 100%;
390+
margin-bottom: 1.5em;
391+
align-items: center;
392+
}
393+
.add-menu-item-row input {
394+
flex: 1;
395+
margin-bottom: 0;
396+
}
397+
.add-menu-item-row button {
398+
margin-bottom: 0;
399+
min-width: 70px;
400+
}
401+
.menu-item {
402+
display: flex;
403+
align-items: center;
404+
justify-content: space-between;
405+
padding: 0.7em 1em;
406+
background: #232e4a;
407+
border-radius: 8px;
408+
margin-bottom: 0.5em;
409+
box-shadow: 0 1.5px 8px #00b4fc22;
410+
}
411+
.menu-item-name {
412+
color: #eaf6ff;
413+
font-weight: 600;
414+
flex: 1;
415+
}
416+
.menu-item-default {
417+
color: #00e6a8;
418+
font-size: 0.9em;
419+
margin-left: 0.5em;
420+
}
421+
.menu-item-remove {
422+
padding: 0.3em 0.8em;
423+
font-size: 0.9em;
424+
background: linear-gradient(90deg, #ff4e6a 0%, #ffb347 100%);
425+
border: none;
426+
border-radius: 6px;
427+
color: #fff;
428+
cursor: pointer;
429+
font-weight: 600;
430+
box-shadow: 0 1.5px 8px rgba(255, 78, 106, 0.3);
431+
transition: background 0.18s, box-shadow 0.18s;
432+
}
433+
.menu-item-remove:hover {
434+
box-shadow: 0 2px 12px rgba(255, 78, 106, 0.5);
435+
}
436+
.menu-item-remove:active {
437+
background: #ff4e6a;
438+
box-shadow: 0 1px 4px rgba(255, 78, 106, 0.4);
439+
}
440+
#contextMenuItems {
441+
width: 100%;
442+
margin-bottom: 1em;
443+
}

package/utils.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ async function sendToWebhook(webhookUrl, data) {
419419
* @param {object} options - Configuration options
420420
* @param {object} options.tab - Tab information
421421
* @param {object} [options.contextInfo] - Context menu info (for right-click)
422+
* @param {string} [options.context] - Context menu name (e.g., "Default", "Automation")
422423
* @param {Function} [options.onProgress] - Progress callback (message) => void
423424
* @param {Function} [options.onSuccess] - Success callback (data) => void
424425
* @param {Function} [options.onError] - Error callback (error) => void
@@ -430,6 +431,7 @@ async function sendToHomeAssistant(options) {
430431
const {
431432
tab,
432433
contextInfo,
434+
context,
433435
onProgress,
434436
onSuccess,
435437
onError,
@@ -515,6 +517,11 @@ async function sendToHomeAssistant(options) {
515517
if (config.deviceName) {
516518
pageInfo.device = config.deviceName;
517519
}
520+
521+
// Add context information if provided (from context menu)
522+
if (context) {
523+
pageInfo.context = context;
524+
}
518525

519526
// Send to webhook
520527
await sendToWebhook(webhookUrl, pageInfo);

0 commit comments

Comments
 (0)