From 46e3f0cb8ecbe79014f67f1aad90b55288024e45 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Thu, 18 Jul 2024 18:01:24 +1000 Subject: [PATCH 01/18] cleanup --- src/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/.DS_Store diff --git a/src/.DS_Store b/src/.DS_Store deleted file mode 100644 index 5008ddfcf53c02e82d7eee2e57c38e5672ef89f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T0 Date: Thu, 18 Jul 2024 18:01:35 +1000 Subject: [PATCH 02/18] formatting --- LICENSE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index 7bed554..a0a0400 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,3 @@ - The MIT License (MIT) Copyright (c) 2013 Glenn 'devalias' Grant (http://devalias.net) From ea6a12f15f8d72f458f4c829dad355ce139b6bac Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Thu, 18 Jul 2024 18:02:39 +1000 Subject: [PATCH 03/18] refactor extension code from mv2 to mv3 --- js/chromeExtensionApiAbstractions.js | 232 --------------------------- manifest.json | 52 +++--- src/bg/background.html | 6 - src/bg/background.js | 85 ---------- src/service_worker.js | 151 +++++++++++++++++ 5 files changed, 176 insertions(+), 350 deletions(-) delete mode 100644 js/chromeExtensionApiAbstractions.js delete mode 100644 src/bg/background.html delete mode 100644 src/bg/background.js create mode 100644 src/service_worker.js diff --git a/js/chromeExtensionApiAbstractions.js b/js/chromeExtensionApiAbstractions.js deleted file mode 100644 index 11ea014..0000000 --- a/js/chromeExtensionApiAbstractions.js +++ /dev/null @@ -1,232 +0,0 @@ -/** - * @overview A collection of Chrome Extension API Abstractions to make my life easier. - * @version 0.1.0 - * @author Glenn 'devalias' Grant - * @copyright Copyright (c) 2013 Glenn 'devalias' Grant (http://devalias.net) - * @license The MIT License (MIT) (see LICENSE.md) - */ - -// ---------------------------------------------------------- -// Chrome API -// ---------------------------------------------------------- - -/** - * Creates a new tab. - * @since 0.1.0 - * @name ChromeExtensionAPI~CreateTab - * @param {ChromeExtensionAPI~CreateTabOptions} createProperties - Options for the Create Tab method. - * @param {ChromeExtensionAPI~CreateTabCallback} callback - Callback for the Create Tab method. - * @see Chrome Extension API - Tabs - Create - */ -function createTab(createProperties, callback) { - chrome.tabs.create(createProperties, callback); -} - -/** - * Gets all tabs that have the specified properties, or all tabs if no properties are specified. - * @since 0.1.0 - * @name ChromeExtensionAPI~QueryTabs - * @param {ChromeExtensionAPI~QueryTabsOptions} queryInfo - Options for the Query Tabs method. - * @param {ChromeExtensionAPI~QueryTabsCallback} callback - Callback for the Query Tabs method. - * @see Chrome Extension API - Tabs - Query - */ -function queryTabs(queryInfo, callback) { - chrome.tabs.query(queryInfo, callback); -} - -/** - * Moves one or more tabs to a new position within its window, or to a new window. - * @since 0.1.0 - * @name ChromeExtensionAPI~MoveTabs - * @param {number|number[]} tabIds - The tab id or list of tab ids to move. - * @param {ChromeExtensionAPI~MoveTabsOptions} moveProperties - Options for the Move Tabs method. - * @param {ChromeExtensionAPI~MoveTabsCallback} callback - Callback for the Move Tabs method. - * @see Chrome Extension API - Tabs - Move - */ -function moveTabs(tabIds, moveProperties, callback) { - chrome.tabs.move(tabIds, moveProperties, callback); -} - -/** - * Creates (opens) a new browser with any optional sizing, position or default URL provided. - * @since 0.1.0 - * @name ChromeExtensionAPI~CreateWindow - * @param {ChromeExtensionAPI~CreateWindowOptions} createData - Options for the Create Window method. - * @param {ChromeExtensionAPI~CreateWindowCallback} callback - Callback for the Create Window method. - * @see Chrome Extension API - Windows - Create - */ -function createWindow(createData, callback) { - chrome.windows.create(createData, callback); -} - -// ---------------------------------------------------------- -// Chrome API - Abstractions -// ---------------------------------------------------------- - -/** Return an array of all tabs for the specified window id. - * @since 0.1.0 - * @param {string} url - The URL to open in the new tab. - * @param {boolean} makeActive - Whether the new tab should become active. - * @see createTab - */ -function createTabWithUrl(url, makeActive) -{ - makeActive = typeof makeActive !== 'undefined' ? makeActive : true; - createTab({ - "url": url, - "active": makeActive - }); -} - -/** Return an array of all tabs for the specified window id. - * @since 0.1.0 - * @param {number} windowId - ID of the window to get tabs from. - * @param {ChromeExtensionAPI~QueryTabsCallback} callback - Array of tabs for window. - * @see queryTabs - */ -function getTabsForWindowId(windowId, callback) { - queryTabs({ - "windowId": windowId - }, - callback); -} - -/** Return an array of all tabs for the parent window of supplied tab. - * @since 0.1.0 - * @param {ChromeExtensionAPI~Tab} tab - Tab to use the parent window of. - * @param {ChromeExtensionAPI~QueryTabsCallback} callback - Array of tabs for parent window. - * @see getTabsForWindowId - */ -function getTabsForParentWindowOfTab(tab, callback) { - getTabsForWindowId(tab.windowId, callback); -} - -/** Return an array of all tab ID's including and to the right of the supplied tab index. - * @since 0.1.0 - * @param {number} tabIndex - Get tabs including and right of this tab index. - * @param {ChromeExtensionAPI~Tab[]} tabs - An array of tab objects to extract the id's from. - * @returns {number[]} Array of tabId's - */ -function getIdsForCurrentAndTabsToRightOf(tabIndex, tabs) { - var tabIds = []; - for (var i = tabIndex; i < tabs.length; i++) { - tabIds[tabIds.length] = tabs[i].id; - } - return tabIds; -} - -/** Return an array of all tab ID's right of the supplied tab index (not including the supplied tab index). - * @since 0.1.0 - * @param {number} tabIndex - Get tabs right of this tab index. - * @param {ChromeExtensionAPI~Tab[]} tabs - An array of tab objects to extract the id's from. - * @returns {number[]} Array of tabId's - * @see getIdsForCurrentAndTabsToRightOf - */ -function getIdsForTabsToRightOf(tabIndex, tabs) { - return getIdsForCurrentAndTabsToRightOf(tabIndex+1, tabs); -} - -/** Create a new window and move the supplied tab ID's to it. - * @since 0.1.0 - * @param {number[]} tabIds - The id's of the tabs to moved. - * @param {ChromeExtensionAPI~MoveTabsCallback} callback - Details about the moved tabs - * @see ChromeExtensionAPI~CreateWindow - * @see moveTabs - */ -function createWindowWithTabs(tabIds, callback) { - if (!tabIds | tabIds.length < 1) - { - alert("There are no tabs to make a new window with."); - return; - } - - createWindow({"tabId": tabIds[0]}, function(newWindow) { - moveTabs(tabIds, {"windowId": newWindow.id, "index": -1}, callback); - }); -} - -// ---------------------------------------------------------- -// End Chrome API - Abstractions -// ---------------------------------------------------------- - -// ---------------------------------------------------------- -// Chrome Extension API - Typedef and Callback doco -// ---------------------------------------------------------- - -// --------- -// Windows -// --------- - -/** A Chrome Extension API - Windows - Window object - * @typedef {object} ChromeExtensionAPI~Window - * @see Chrome Extension API - Windows - Window - */ - -/** Options for Chrome Extension API - Windows - Create Windows method - * @typedef {object} ChromeExtensionAPI~CreateWindowOptions - * @see ChromeExtensionAPI~CreateWindow - * @see Chrome Extension API - Windows - Create Window Options - */ - -/** - * Callback function for Chrome Extension API - Windows - Create method - * @callback ChromeExtensionAPI~CreateWindowCallback - * @param {ChromeExtensionAPI~Window} window - Contains details about the created window - * @see ChromeExtensionAPI~CreateWindow - * @see Chrome Extension API - Windows - Create - */ - -// --------- -// Tabs -// --------- - -/** A Chrome Extension API - Tabs - Tab object - * @typedef {object} ChromeExtensionAPI~Tab - * @see Chrome Extension API - Tabs - Tab - */ - -/** Options for Chrome Extension API - Tabs - Create method - * @typedef {object} ChromeExtensionAPI~CreateTabOptions - * @see ChromeExtensionAPI~CreateTab - * @see Chrome Extension API - Tabs - Create - */ - -/** - * Callback function for Chrome Extension API - Tabs - Create method. - * @callback ChromeExtensionAPI~CreateTabCallback - * @param {ChromeExtensionAPI~Tab} tab - Details of the created tab. - * @see ChromeExtensionAPI~CreateTab - * @see Chrome Extension API - Tabs - Create - */ - -/** Options for Chrome Extension API - Tabs - Query method - * @typedef {object} ChromeExtensionAPI~QueryTabsOptions - * @see ChromeExtensionAPI~QueryTab - * @see Chrome Extension API - Tabs - Query - */ - -/** - * Callback function for Chrome Extension API - Tabs - Query method. - * @callback ChromeExtensionAPI~QueryTabsCallback - * @param {ChromeExtensionAPI~Tab[]} tabs - Results for the queried tabs. - * @see ChromeExtensionAPI~QueryTab - * @see Chrome Extension API - Tabs - Query - */ - -/** Options for Chrome Extension API - Tabs - Move method - * @typedef {object} ChromeExtensionAPI~MoveTabsOptions - * @see ChromeExtensionAPI~MoveTab - * @see Chrome Extension API - Tabs - Move - */ - -/** - * Callback function for Chrome Extension API - Tabs - Move method. - * @callback ChromeExtensionAPI~MoveTabsCallback - * @param {ChromeExtensionAPI~Tab|ChromeExtensionAPI~Tab[]} tabs - Details about the moved tabs. - * @see ChromeExtensionAPI~MoveTab - * @see Chrome Extension API - Tabs - Move - */ - -// ---------------------------------------------------------- -// End Chrome Extension API - Typedef and Callback doco -// ---------------------------------------------------------- diff --git a/manifest.json b/manifest.json index 404abe8..e61dc01 100644 --- a/manifest.json +++ b/manifest.json @@ -1,42 +1,40 @@ { + "manifest_version": 3, + "minimum_chrome_version": "110", + "version": "2.0.0", "name": "New Window With Tabs To Right", - "version": "1.0.1", - "manifest_version": 2, "description": "This extension creates a new window with the tabs to the right of the currently selected tab.", "homepage_url": "http://devalias.net/dev/chrome-extensions/", - "content_security_policy": "script-src 'self' https://ssl.google-analytics.com; object-src 'self'", - "permissions": [ - "tabs", - "contextMenus" - ], + "default_locale": "en", "icons": { "16": "icons/NewWindowWithTabsToRight-Icon@16px.png", "48": "icons/NewWindowWithTabsToRight-Icon@48px.png", "128": "icons/NewWindowWithTabsToRight-Icon@128px.png" }, - "default_locale": "en", + "permissions": [ + "contextMenus" + ], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'" + }, "background": { - "scripts": [ - "js/googleAnalytics.js", - "js/chromeExtensionApiAbstractions.js", - "src/bg/background.js" - ], - "persistent": true + "service_worker": "src/service_worker.js", + "type": "module" }, "commands": { - "newWindowWithCurrentAndTabsToRight": { - "suggested_key": { - "default": "Ctrl+Shift+Y", - "mac": "Command+Shift+Y" - }, - "description": "Create a new window with the current tab and tabs on the right." + "newWindowWithCurrentAndTabsToRight": { + "suggested_key": { + "default": "Ctrl+Shift+Y", + "mac": "Command+Shift+Y" + }, + "description": "Create a new window with the current tab and tabs on the right." + }, + "newWindowWithTabsToRight": { + "suggested_key": { + "default": "Ctrl+Shift+U", + "mac": "Command+Shift+U" }, - "newWindowWithTabsToRight": { - "suggested_key": { - "default": "Ctrl+Shift+U", - "mac": "Command+Shift+U" - }, - "description": "Create a new window with the tabs on the right." - } + "description": "Create a new window with the tabs on the right." + } } } diff --git a/src/bg/background.html b/src/bg/background.html deleted file mode 100644 index 1ce1e93..0000000 --- a/src/bg/background.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/bg/background.js b/src/bg/background.js deleted file mode 100644 index 751c7c9..0000000 --- a/src/bg/background.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @overview Handles the background functionality for the 'New Window With Tabs To Right' extension. - * @version 0.0.1 - * @author Glenn 'devalias' Grant - * @copyright Copyright (c) 2013 Glenn 'devalias' Grant (http://devalias.net) - * @license The MIT License (MIT) (see LICENSE.md) - */ - -// Menu Actions -function newWindowWithTabsToRight(info, currentTab) { - _gaq.push(['_trackEvent', 'contextMenu', 'clicked', 'newWindowWithTabsToRight']); - getTabsForParentWindowOfTab(currentTab, function(tabs) { - var tabIds = getIdsForTabsToRightOf(currentTab.index, tabs); - createWindowWithTabs(tabIds, function(tabs) { - // TODO: Maybe ensure tabs were moved? - }); - }); -} - -function newWindowWithCurrentAndTabsToRight(info, currentTab) { - _gaq.push(['_trackEvent', 'contextMenu', 'clicked', 'newWindowWithCurrentAndTabsToRight']); - getTabsForParentWindowOfTab(currentTab, function(tabs) { - var tabIds = getIdsForCurrentAndTabsToRightOf(currentTab.index, tabs); - createWindowWithTabs(tabIds, function(tabs) { - // TODO: Maybe ensure tabs were moved? - }); - }); -} - -function aboutTheDeveloper(info, currentTab) { - _gaq.push(['_trackEvent', 'contextMenu', 'clicked', 'aboutTheDeveloper']); - createTabWithUrl("http://devalias.net/dev/chrome-extensions/new-window-with-tabs-to-right/", true); -} - -// Keybinding handlers -chrome.commands.onCommand.addListener(function(command) { - var qOptions = {currentWindow: true, active: true} - chrome.tabs.query(qOptions, function(arrayOfTabs) { - var curTab = arrayOfTabs[0]; - if (command == "newWindowWithTabsToRight") { - newWindowWithTabsToRight(qOptions, curTab); - } - else if (command == "newWindowWithCurrentAndTabsToRight") { - newWindowWithCurrentAndTabsToRight(qOptions, curTab); - } - }); -}); - -// Menu -// TODO: Abstract the chrome API stuff into library -var menuRoot = chrome.contextMenus.create({ - "type": "normal", - "title": "New window with..", - "contexts": ["page"] -}); - -var menuWithTabsToRight = chrome.contextMenus.create({ - "type": "normal", - "parentId": menuRoot, - "title": "..tabs to right", - "contexts": ["page"], - "onclick": newWindowWithTabsToRight -}); - -var menuWithThisTabAndTabsToRight = chrome.contextMenus.create({ - "type": "normal", - "parentId": menuRoot, - "title": "..this tab and tabs to right", - "contexts": ["page"], - "onclick": newWindowWithCurrentAndTabsToRight -}); - -var menuSeparator = chrome.contextMenus.create({ - "type": "separator", - "parentId": menuRoot, - "contexts": ["page"], -}); - -var menuAboutTheDeveloper = chrome.contextMenus.create({ - "type": "normal", - "parentId": menuRoot, - "title": "About the Developer", - "contexts": ["page"], - "onclick": aboutTheDeveloper -}); diff --git a/src/service_worker.js b/src/service_worker.js new file mode 100644 index 0000000..344256e --- /dev/null +++ b/src/service_worker.js @@ -0,0 +1,151 @@ +/** + * Handles the extension's installation event. + * Sets up context menus. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/runtime#event-onInstalled} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#method-create} + * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu} + */ +chrome.runtime.onInstalled.addListener(() => { + const menuContexts = ["page"]; + + const menuRoot = chrome.contextMenus.create({ + contexts: menuContexts, + title: "New window with.." + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: newWindowWithCurrentAndTabsToRight.name, + title: "..this tab and tabs to right" + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: newWindowWithTabsToRight.name, + title: "..tabs to right" + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + type: "separator" + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: aboutTheDeveloper.name, + title: "About the Developer" + }); +}); + +/** + * Handles context menu item clicks. + * + * @param {object} info - Information about the item clicked and the context where the click happened. + * @param {object} tab - The details of the tab where the click happened. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#event-onClicked} + */ +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + const handlers = { + newWindowWithCurrentAndTabsToRight, + newWindowWithTabsToRight, + aboutTheDeveloper, + }; + + await handlers[info.menuItemId]?.(tab); +}); + +/** + * Handles keyboard command execution. + * + * @param {string} command - The name of the command. + * @param {object} tab - The details of the tab where the command was executed. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/commands#event-onCommand} + */ +chrome.commands.onCommand.addListener(async (command, tab) => { + const handlers = { + newWindowWithCurrentAndTabsToRight, + newWindowWithTabsToRight, + }; + + await handlers[command]?.(tab); +}); + +/** + * Opens a new window with the current tab and tabs to the right. + * + * @param {object} tab - The details of the current tab. + */ +async function newWindowWithCurrentAndTabsToRight(tab) { + const tabIds = await getTabIdsToMove({ tab, includeCurrentTab: true }); + await createWindowWithTabs(tabIds); +} + +/** + * Opens a new window with tabs to the right of the current tab. + * + * @param {object} tab - The details of the current tab. + */ +async function newWindowWithTabsToRight(tab) { + const tabIds = await getTabIdsToMove({ tab, includeCurrentTab: false }); + await createWindowWithTabs(tabIds); +} + +/** + * Opens a new tab with information about the developer. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#method-create} + */ +async function aboutTheDeveloper() { + chrome.tabs.create({ url: "http://devalias.net/dev/chrome-extensions/new-window-with-tabs-to-right/", active: true }); +} + +/** + * Gets the IDs of tabs to move based on the current tab and whether to include the current tab. + * + * @param {object} options - Options for getting tab IDs. + * @param {object} options.tab - The details of the current tab. + * @param {boolean} options.includeCurrentTab - Whether to include the current tab. + * @returns {Promise} A promise that resolves to an array of tab IDs. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#method-query} + */ +async function getTabIdsToMove({ tab, includeCurrentTab }) { + let currentTab = tab; + + if (!currentTab) { + const [activeTab] = await chrome.tabs.query({ currentWindow: true, active: true }); + currentTab = activeTab; + } + + if (!currentTab || typeof currentTab.index === 'undefined' || typeof currentTab.windowId === 'undefined') return []; + + const tabs = await chrome.tabs.query({ windowId: currentTab.windowId }); + if (!tabs || tabs.length === 0) return []; + + const startIndex = includeCurrentTab ? currentTab.index : currentTab.index + 1; + return tabs.filter(t => t.index >= startIndex).map(t => t.id); +} + +/** + * Creates a new window with the specified tabs. + * + * @param {number[]} tabIds - The IDs of the tabs to move to the new window. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/windows#method-create} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#method-move} + */ +async function createWindowWithTabs(tabIds) { + if (!tabIds || tabIds.length === 0) return; + + const newWindow = await chrome.windows.create({ tabId: tabIds[0] }); + if (tabIds.length > 1) { + await chrome.tabs.move(tabIds.slice(1), { windowId: newWindow.id, index: -1 }); + } +} From d999969a24355563285f1b777ac46d32b638bffe Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 15:51:59 +1000 Subject: [PATCH 04/18] add 32x32px icon --- icons/NewWindowWithTabsToRight-Icon@32px.png | Bin 0 -> 2195 bytes manifest.json | 1 + 2 files changed, 1 insertion(+) create mode 100644 icons/NewWindowWithTabsToRight-Icon@32px.png diff --git a/icons/NewWindowWithTabsToRight-Icon@32px.png b/icons/NewWindowWithTabsToRight-Icon@32px.png new file mode 100644 index 0000000000000000000000000000000000000000..55bea639ce7b02036b8713b9f0e5e307153decb5 GIT binary patch literal 2195 zcmZuy3piB!7T;q$$D{GQIAY9rmCShMF%9?7DEcrAGTsJ53Ny`&Ov*F|2YIEb&+Dp2 z7o7;jrKV1($*a=wj6Pg^B#y&1gjBeDaH`uq|NX7K)_?8aZ>|4YYwvGg^l(2a57UGJ z03h$;j3tsClI|3vTi$B$r4xH()<#O9K)RB>*adfENJK z1SGa?05}Hu;fqa#m;qv-UlA1l^k_hQ7ZU?&(Q*a_WbbH)2x=Gw;Sx)Zq7W(Mh_j#{ z%7c8(!4vz!NARFuZ4d|SBS~9;N`~g_#{d9XWsyLD;u3WL5HpGPCNW8FuGnyDESfAz zf@a0iL@WTu!h&Wjg-J%RVq@YMSe6}f8-WFFksE_VY(to*?2sfk55!?Aor17Jo1#sT z_Amqjful!6Vu|=8yL518hdjY#(y$mzLP7#M!2(UCM`6rtZEZ28<`{Ex6A)p-I2*?# zvrOU`sGT5x#=%n<;q+)4GnyKQ5XB{jQRA6*NTevym$iG&SlX9Fag5!n!1^#OG7V#f zHpToEi5VUF^?3io5@U3%~fWwFy`b~p(rnl_{$7c@% zYx(n4u!n`t5#9iRM34*K!J8yAf85Pk&lg@ZXxCT$D%%1Jk=i;kIF=``E}kMTCyA3z zyd*_{4hhXS5`9*hK2`=p&&TMfL-L%D$l_Yt%O>@`(M6Og0X?BKmRI&1yjzF9TD z;#zE5`e=$cMYaGgo%-QCQ_e;KxG8Di&RsF=9v+sBtS@@4%qr-GBy;`UUm1G%`N?Q#XpH$!*hQ;o8B@nI@s6Wiha5e0e{_Xk(`j3T$ z(^514I(P1zsg+e*_~XZqyCyf=m)`}>x3x(Y7#kTO^e?}COR?XwdTL-p^U-@6s2QS= zF32idCa;MRFpUij5V{x|gCUd25Z&6NP{7#Oczh+Xw5jPjwH$u6pde*>udcq{ z3GZYm&h;ud6k7IdeVxyUi(3fDY7KKb48i%4{rvbAoPerS?N74SBZvY zJaEXgUXXY+`l`LWe2w{!E(4x+bz|e|Z(Ut(d(fuz@l2_}++ayDaenUEjoI#Q51s2; zm6eq(Mb~n)PjTXbGiT25YHKeI0`jtXYTcpJwHok3!2ZUi!IaKUm+pJrC;fpwb8&Vv zOjT8NOHM&SyOeE9k%q@LHa2Rq*Oj~!_I~`lnWWYupe6o3(J#;Q+@CR#p}^H6kDShy zaN+7}7xUD_ROSlF`n(?KgKA@bbj)t zU!0#+Gj>$KRY#;2zuu>u(Zy96bq`O}>wHPB@OOxBnPobAlf>32yJ3x zV+Z)lFQhBBrtS@CeFT^F6o-s@qmj$;l!CKY-i5H`rl#{>kodyp1AaJ7-(#MhDmFTD zGMNeIUn=UV_4bDz&o^1!it1>)pzI2MT@{Z4S23;z0a#d2mA~)@YLDS)#AoVz{pYU( zVm^*JDZvqQ|H&K(vJ><*W4H+iH%^bu!$173uP6uBJ2i7n^dEL{a>v&=h9>_PWo~Qd literal 0 HcmV?d00001 diff --git a/manifest.json b/manifest.json index e61dc01..ff027e1 100644 --- a/manifest.json +++ b/manifest.json @@ -8,6 +8,7 @@ "default_locale": "en", "icons": { "16": "icons/NewWindowWithTabsToRight-Icon@16px.png", + "32": "icons/NewWindowWithTabsToRight-Icon@32px.png", "48": "icons/NewWindowWithTabsToRight-Icon@48px.png", "128": "icons/NewWindowWithTabsToRight-Icon@128px.png" }, From bdf1a1599aa57a35a3b27b15a24f84b6f8517136 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 15:59:17 +1000 Subject: [PATCH 05/18] remove legacy TODO notes --- TODO.md | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index ff9c328..0000000 --- a/TODO.md +++ /dev/null @@ -1,10 +0,0 @@ -### TODO - -* LAUNCH! -* Document all functions with JSDoc -* Abstract the rest of the API stuff into seperate/reusable file -* Add an 'options' page with my info (name, website, blog, github, etc. donations?) - * Feedback/suggestions/comments/criticisms/binary love notes? => feedback@devalias.net - * Mention why I created this? (eg. For sesh, ping the developer or something?) - * New sesh with: tabs to right, current and tabs to right -* Allow moving tabs to right to specified window ('spin through all open windows, display as submenu') From e3e0d619b30c9310c4de1a721f6f47c36eead0d8 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 16:59:37 +1000 Subject: [PATCH 06/18] optimize icons/*.png with oxipng oxipng -o max --dir oxipng --alpha *.png --- icons/NewWindowWithTabsToRight-Icon@128px.png | Bin 7787 -> 7227 bytes icons/NewWindowWithTabsToRight-Icon@16px.png | Bin 593 -> 575 bytes icons/NewWindowWithTabsToRight-Icon@32px.png | Bin 2195 -> 2066 bytes icons/NewWindowWithTabsToRight-Icon@48px.png | Bin 2143 -> 2027 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/icons/NewWindowWithTabsToRight-Icon@128px.png b/icons/NewWindowWithTabsToRight-Icon@128px.png index 8ce792d3483591049a2a6c2b171e55cdec8697fa..d941fae3d3abec3cbc755dfe435c97a1dd6251cb 100644 GIT binary patch literal 7227 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 Vosa@D>I?t?002ovPDHLkV1o04`uhL? diff --git a/icons/NewWindowWithTabsToRight-Icon@32px.png b/icons/NewWindowWithTabsToRight-Icon@32px.png index 55bea639ce7b02036b8713b9f0e5e307153decb5..5e5c04e11fef2657687f477f8a6c3f485ecdfdb1 100644 GIT binary patch delta 1283 zcmV+e1^oJx5t0zFAOn8{+(|@1RCwC$S4~JZ)pk^!6F5V z1s8(u>%x_gt^@^B8tKM`%M>Z%LU30NXbFj>g1V|T3pH9W3JTGRg8%V%oOy4?d9UYO z9I;b7u_!cwd~o5N_pbMx^PTUU>)-?aZwSY6-ns`j3+KIJz}SD>+|1K7&7gfYq7Vv& za8;b1o=U?oEU(wQ1i-=>^cb`|?7X|X`}35P6f-wBR|JDWY;0^mN(tMxvA@3$es>jE ze!pW25#QY0ECI~jy?a;r&=CXSaM(35F<}l34eghemTKOtEPlt?fI%7YE?Cwnzdy4< zwgqAhx~@mE$KHR=PWVpFJr6Ny-~x2Sh7%tjF92wh)6){u)3^>O0U2FN9altseLb3* znvh6Y?h^G=Q&Xs}t_Fjj4-O7cRaFJX=;$c2v$Ii@U!)lN>B$qcx3?oME)E3op8{Yi z8MkkLgyQ02Bqk=R0JgTal+D=M-9_S?H|O^J{5+|PeEG{mpeIzulzdbWE z1Hz?()ejF3qlZK|Vt72BLlTz{z%gZ~H8eDUG=Y{vcvM96cf`USCZEdYt zT3RZohY@EOh$m-xd3nglxD5u^`{?M1&jEqxzvm<;Crh{69V{;||7~e$X&-=~09Xf? zmOuqSef@E7Z*M0Bfs&FEWTdB4#6i#;!0PHMIy-+m)eHzy7C|eBvERoag@n)NQ}m9E zjKFc2YKz+P6oG(=qN1WddwP1d0T`E-Agb@h{QO+_<;!Ww0eyP^zObDz_V)HwgcdpF zE(+TYh$Sf7w&5~dsx+Zs2to*Gx}n6u@6roL zFx`LK+uJq(Ue4m{*EXV{cXV`I>~+peId5!i6s@hT@EsrG#2>(`S97SS_>#|2ZAN-Q zdHEOW>=!S_k(G6aw2%WS1^WB@MN3NyYHDgwS664yL`(?Twh<>`xZSRpZOAM{%AwSr z;7%gSq1wtYT>PEsW;Ii=ItXm@B0G+0=DmMB2S}tDC@U-TlQ@s48w z$111g2Xzk51>Q^FPw@Eh4}fZK4~sJ5Loi~BF#4`%XDlSYX)0%aa>nOTRv=21a|VAu z^Im?>;0}nCGYU?u0C;Q)cFgFd3*_ggPvK5T#P;qEq!4)c?L%V7z~RAxs`aDq9&uij zxlaY0mX=21^icqT^T47JGXPSnBQ-%+upvl54$XIATBaIEQcBh57G=qUiP@y@XSGgJ z|Df<*`dgwt#o6226S}V9DqyN42+MoR%N03y3YexFS?*+yv#_x6Rm8`?GSJr67OA`0 z@#FVaW@e!Qp*cca8mwwjO_QvLi|^HnKoCxYW+*K!*+LA6wY4=|K!Vqs`kAgLCjhW< tJqQ5o{0{*B_v(j<#LdF~@cZwb#$V-Qd(e}O!pr~w002ovPDHLkV1gY6X)OQ% delta 1413 zcmV;01$z3D5R(zGAOn8|T}ebiR9Fe^R!vAGR}ik(J=3kuj59g`2meOb-;fO$1cL+& z20RFQj|We}<|HV%AuG9f@VFra@gR7ME@)VG4H49fF)sMC8gLa9RwD}jujA<0)6?_Q zsd{4%Gvlr!ZpdEtL3h8OdR6tl`l?<7d~6>W0hY?XuInjfQt5w)UuTTz@#7EPzwvZ< zdimhsphOf!8T*lBZ-c>L^3*%Wp-@PWWjWw-xi%4nfFWoyI`LnEouDG)0|NtJW@cup z#l^);(=^!M-v>buAQFjy$KwH-yG;uyiXLGst+TUJK>1a}##)YKFd78XK9X$6kM@avN&(A(P!X0w^r6L^=>0fK*$T29UNJhD<~)+C6VRjWj_Bl!X`Ly zRV2b(8ee}7hr^4c^I`V_PcOE$wE=PhE-x=(VPOFwuSWv_D=RDD^?ISczMgNT5R{jf zgWK%}RaMinv$I7+MJEK3QNgs;a8MnVpRuYk+kCc6N54udk2S01XR}7SLekuNe@S06w3O*MrH) zNzk#-YFYr(G6a6V3KbO<{|pWe9-)3YWe5;%wzjq!e)$qp4juaZ{(TnF!*F`);nWSF zQ7V6k;fM~BBw^ffj0H)OB_5if76c3ucqlhoGmYC$=3=#*MePx3liT~ zSNApP0ZOxriwmf&{fg^;@nRP8^71kF-57r&1|uUQtgEXFnwpxRrKLs2ikKdZL}ar7 zvc)3BY*EMHKx`5T$@^4tiS6kYD?$!Dtz?9n)+rN+&5{(|PGfo`$;@G2yL( zO^t%&JjZOXvT*oJk}TUXW7xUofI*c0{(hk6eVWy3(fxk^Vo_1ijFD-EzN?f6B3pk# zBXUvu?!g1V+mNHMiy1IHJOqlO@MmIT0>{Ig<$k{cm^r*I5MqjQ36CHD#C6#14kD#t zBgQShc=fu5fU!srlR+YqqN?1Og2qT6o{9<#eibfGeUhblYLo%gjBxTn^c9V7+(d!{ zST>nasfO?Z`R(abu%u_e(aAC9JA;3R-#^4S%izUQOeyu~heybDhA8M3AGbE6X_c~MYG*po2Cxr4mfTyIwMEOqlJ~gY=%70Ku7WZOB z%+ya$PnpRSy~t8IK~V8HZEbDUx%0~irvK!z*c|%$`uaEIfMFxq8IFKa2M~RJ_2*xH z4CLmP%UHSbzbsii8dpkDQ4}~I3<-*Ne9RFc7ShyUR+cUH|7Xa?e-ljX?(U|$T=p+a zCR;iRU8`F08*kAk-DLc4^d-vHTWtE@MG_Do>+g!yT|L9ozb}$^HT`kl?GX4E)0vG5 T(&Xr!00000NkvXXu0mjfccG|Y diff --git a/icons/NewWindowWithTabsToRight-Icon@48px.png b/icons/NewWindowWithTabsToRight-Icon@48px.png index d5702a173dd7f5438c51581218f3250baec8f258..91f94973db7a972ab401be4f11ec6f8e8a81d154 100644 GIT binary patch delta 1977 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 Date: Fri, 19 Jul 2024 17:09:01 +1000 Subject: [PATCH 07/18] remove unused _locales/ (i18n) --- _locales/en/messages.json | 3 --- manifest.json | 1 - 2 files changed, 4 deletions(-) delete mode 100644 _locales/en/messages.json diff --git a/_locales/en/messages.json b/_locales/en/messages.json deleted file mode 100644 index 803bf03..0000000 --- a/_locales/en/messages.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - /* Localisation */ -} diff --git a/manifest.json b/manifest.json index ff027e1..28256ef 100644 --- a/manifest.json +++ b/manifest.json @@ -5,7 +5,6 @@ "name": "New Window With Tabs To Right", "description": "This extension creates a new window with the tabs to the right of the currently selected tab.", "homepage_url": "http://devalias.net/dev/chrome-extensions/", - "default_locale": "en", "icons": { "16": "icons/NewWindowWithTabsToRight-Icon@16px.png", "32": "icons/NewWindowWithTabsToRight-Icon@32px.png", From 399d9c9bd6456430dafe9866dc4b5e998a9ef09b Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 18:06:01 +1000 Subject: [PATCH 08/18] add build / clean scripts + improve .gitignore --- .gitignore | 3 +++ scripts/build | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++ scripts/clean | 48 +++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100755 scripts/build create mode 100755 scripts/clean diff --git a/.gitignore b/.gitignore index fd5106f..c9d053e 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ .DS_STORE + +dist/ +*.zip diff --git a/scripts/build b/scripts/build new file mode 100755 index 0000000..89b5fd7 --- /dev/null +++ b/scripts/build @@ -0,0 +1,75 @@ +#!/usr/bin/env zsh + +# Capture script name +SCRIPT_NAME=$0 + +# Variables +DIST_DIR="dist" +ITEMS_TO_COPY=("README.md" "LICENSE.md" "manifest.json" "src" "icons") +EXTENSION_NAME="newWindowWithTabsToRight" + +# Function to show help +show_help() { + echo "Usage: $SCRIPT_NAME [options]" + echo "Options:" + echo " -h, --help Show this help message" +} + +# Function to read version from manifest.json using jq +read_version() { + if ! command -v jq &> /dev/null; then + echo "Error: jq is not installed." + exit 1 + fi + + VERSION=$(jq -r '.version' manifest.json) + if [ -z "$VERSION" ]; then + echo "Error: Could not read version from manifest.json" + exit 1 + fi +} + +# Parse options +while [[ "$#" -gt 0 ]]; do + case $1 in + -h|--help) show_help; exit 0;; + *) echo "Unknown option: $1"; show_help; exit 1;; + esac + shift +done + +# Read version +echo "Reading version from manifest.json..." +read_version +echo "Version: $VERSION" + +# Create zip filename +ZIP_FILE="${EXTENSION_NAME}-${VERSION}.zip" + +# Create the dist directory +echo "Creating dist directory..." +mkdir -p $DIST_DIR + +# Copy files and directories using rsync to exclude .DS_Store files +echo "Copying files and directories..." +for item in ${ITEMS_TO_COPY[@]}; do + if [ -e $item ]; then + rsync -a --exclude='.DS_Store' $item $DIST_DIR/ + echo " Copied: $item" + else + echo " Warning: $item does not exist." + fi +done + +# Create zip file +if [ -f $ZIP_FILE ]; then + echo "Removing existing zip file: $ZIP_FILE" + rm $ZIP_FILE +fi + +echo "Creating zip file: $ZIP_FILE" +cd $DIST_DIR +zip -r ../$ZIP_FILE ./* +cd .. + +echo "Build completed. Files are in the $DIST_DIR directory and zipped in $ZIP_FILE." diff --git a/scripts/clean b/scripts/clean new file mode 100755 index 0000000..1ff7a14 --- /dev/null +++ b/scripts/clean @@ -0,0 +1,48 @@ +#!/usr/bin/env zsh + +# Capture script name +SCRIPT_NAME=$0 + +# Variables +DIST_DIR="dist" +EXTENSION_NAME="newWindowWithTabsToRight" + +# Function to show help +show_help() { + echo "Usage: $SCRIPT_NAME [options]" + echo "Options:" + echo " -h, --help Show this help message" +} + +# Parse options +while [[ "$#" -gt 0 ]]; do + case $1 in + -h|--help) show_help; exit 0;; + *) echo "Unknown option: $1"; show_help; exit 1;; + esac + shift +done + +# Remove the dist directory +if [ -d ./$DIST_DIR ]; then + echo "Removing dist directory: $DIST_DIR" + rm -rf ./$DIST_DIR +else + echo "Dist directory does not exist: $DIST_DIR" +fi + +# Find and remove the zip files with NULL_GLOB enabled just for this line +ZIP_FILES=($(setopt NULL_GLOB; echo ./${EXTENSION_NAME}-*.zip)) + +# Remove the zip files +if [ ${#ZIP_FILES[@]} -gt 0 ]; then + echo "Removing zip files:" + for file in $ZIP_FILES; do + echo " $file" + rm -f "$file" + done +else + echo "No zip files found matching: ${EXTENSION_NAME}-*.zip" +fi + +echo "Cleanup completed. $DIST_DIR directory and matching zip files in the current directory have been removed." From fbd844bd1a5a97dd3827fa1faba0d2c1949d31a7 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 18:06:34 +1000 Subject: [PATCH 09/18] fix: ensure all contextMenus have id's set --- src/service_worker.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/service_worker.js b/src/service_worker.js index 344256e..04d2e4c 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -11,6 +11,7 @@ chrome.runtime.onInstalled.addListener(() => { const menuRoot = chrome.contextMenus.create({ contexts: menuContexts, + id: 'rootContextMenu', title: "New window with.." }); @@ -31,6 +32,7 @@ chrome.runtime.onInstalled.addListener(() => { chrome.contextMenus.create({ contexts: menuContexts, parentId: menuRoot, + id: 'contextMenu-separator', type: "separator" }); From 75e45c2c8042a563d90fbdccac68d65b50efbc03 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 18:10:20 +1000 Subject: [PATCH 10/18] add optimize-icons script --- scripts/optimize-icons | 51 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100755 scripts/optimize-icons diff --git a/scripts/optimize-icons b/scripts/optimize-icons new file mode 100755 index 0000000..d53f97e --- /dev/null +++ b/scripts/optimize-icons @@ -0,0 +1,51 @@ +#!/usr/bin/env zsh + +# Capture script name +SCRIPT_NAME=$0 + +# Variables +ICONS_DIR="icons" + +# Function to show help +show_help() { + echo "Usage: $SCRIPT_NAME [options]" + echo "Options:" + echo " -h, --help Show this help message" +} + +# Parse options +while [[ "$#" -gt 0 ]]; do + case $1 in + -h|--help) show_help; exit 0;; + *) echo "Unknown option: $1"; show_help; exit 1;; + esac + shift +done + +# Check if oxipng is installed +if ! command -v oxipng &> /dev/null; then + echo "oxipng is not installed." + read "response?Would you like to install it with Homebrew? (y/n) " + if [[ "$response" == "y" || "$response" == "Y" ]]; then + if command -v brew &> /dev/null; then + echo "Installing oxipng with Homebrew..." + brew install oxipng + else + echo "Homebrew is not installed. Please install Homebrew first: https://brew.sh/" + exit 1 + fi + else + echo "Please install oxipng with Homebrew: brew install oxipng" + exit 1 + fi +fi + +# Optimize PNG files in the icons directory +if [ -d $ICONS_DIR ]; then + echo "Optimizing PNG files in the $ICONS_DIR directory..." + oxipng -o max --dir $ICONS_DIR --alpha $ICONS_DIR/*.png + echo "Optimization completed." +else + echo "Icons directory does not exist: $ICONS_DIR" + exit 1 +fi From b3bbe3b394004a4ff27f423fb1c6baeb038cab47 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 19:07:26 +1000 Subject: [PATCH 11/18] update legacy ua analytics to ga4 --- js/googleAnalytics.js | 28 --------- manifest.json | 3 +- src/google-analytics.js | 127 ++++++++++++++++++++++++++++++++++++++++ src/service_worker.js | 18 +++++- 4 files changed, 146 insertions(+), 30 deletions(-) delete mode 100644 js/googleAnalytics.js create mode 100644 src/google-analytics.js diff --git a/js/googleAnalytics.js b/js/googleAnalytics.js deleted file mode 100644 index 415b49b..0000000 --- a/js/googleAnalytics.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * @overview Google Analytics asynchronous load - * @see http://developer.chrome.com/extensions/tut_analytics.html - * @see https://developers.google.com/analytics/devguides/collection/gajs/eventTrackerGuide - */ - -var _gaq = _gaq || []; -_gaq.push(['_setAccount', 'UA-42943200-1']); // NOTE: Make sure you change this to your own Google Analytics code!!! -_gaq.push(['_trackPageview']); - -(function() { - var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; - ga.src = 'https://ssl.google-analytics.com/ga.js'; - var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); -})(); - - -// Note: This was the code provided when I setup Google Analytics. Newer version of? -// diff --git a/manifest.json b/manifest.json index 28256ef..7d39bde 100644 --- a/manifest.json +++ b/manifest.json @@ -12,7 +12,8 @@ "128": "icons/NewWindowWithTabsToRight-Icon@128px.png" }, "permissions": [ - "contextMenus" + "contextMenus", + "storage" ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" diff --git a/src/google-analytics.js b/src/google-analytics.js new file mode 100644 index 0000000..4c5d837 --- /dev/null +++ b/src/google-analytics.js @@ -0,0 +1,127 @@ +// See: +// https://developer.chrome.com/docs/extensions/how-to/integrate/google-analytics-4 +// https://developers.google.com/analytics/devguides/collection/protocol/ga4 +// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag +// https://github.com/GoogleChrome/chrome-extensions-samples/tree/main/functional-samples/tutorial.google-analytics +// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/scripts/google-analytics.js +// https://github.com/GoogleChrome/chrome-extensions-samples/blob/main/functional-samples/tutorial.google-analytics/service-worker.js + +const GA_ENDPOINT = 'https://www.google-analytics.com/mp/collect'; +const GA_DEBUG_ENDPOINT = 'https://www.google-analytics.com/debug/mp/collect'; + +// Get via https://analytics.google.com/analytics/web/ +const MEASUREMENT_ID = 'G-BCGJGLETYW'; +const API_SECRET = '_xmXa00ATSmnNWs2J2xadQ'; +const DEFAULT_ENGAGEMENT_TIME_MSEC = 100; + +// Duration of inactivity after which a new session is created +const SESSION_EXPIRATION_IN_MIN = 30; + +class Analytics { + constructor(debug = false) { + this.debug = debug; + } + + // Returns the client id, or creates a new one if one doesn't exist. + // Stores client id in local storage to keep the same client id as long as + // the extension is installed. + async getOrCreateClientId() { + let { clientId } = await chrome.storage.local.get('clientId'); + if (!clientId) { + // Generate a unique client ID, the actual value is not relevant + clientId = self.crypto.randomUUID(); + await chrome.storage.local.set({ clientId }); + } + return clientId; + } + + // Returns the current session id, or creates a new one if one doesn't exist or + // the previous one has expired. + async getOrCreateSessionId() { + // Use storage.session because it is only in memory + let { sessionData } = await chrome.storage.session.get('sessionData'); + const currentTimeInMs = Date.now(); + // Check if session exists and is still valid + if (sessionData && sessionData.timestamp) { + // Calculate how long ago the session was last updated + const durationInMin = (currentTimeInMs - sessionData.timestamp) / 60000; + // Check if last update lays past the session expiration threshold + if (durationInMin > SESSION_EXPIRATION_IN_MIN) { + // Clear old session id to start a new session + sessionData = null; + } else { + // Update timestamp to keep session alive + sessionData.timestamp = currentTimeInMs; + await chrome.storage.session.set({ sessionData }); + } + } + if (!sessionData) { + // Create and store a new session + sessionData = { + session_id: currentTimeInMs.toString(), + timestamp: currentTimeInMs.toString() + }; + await chrome.storage.session.set({ sessionData }); + } + return sessionData.session_id; + } + + // Fires an event with optional params. Event names must only include letters and underscores. + async fireEvent(name, params = {}) { + // Configure session id and engagement time if not present, for more details see: + // https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag#recommended_parameters_for_reports + if (!params.session_id) { + params.session_id = await this.getOrCreateSessionId(); + } + if (!params.engagement_time_msec) { + params.engagement_time_msec = DEFAULT_ENGAGEMENT_TIME_MSEC; + } + + try { + const response = await fetch( + `${ + this.debug ? GA_DEBUG_ENDPOINT : GA_ENDPOINT + }?measurement_id=${MEASUREMENT_ID}&api_secret=${API_SECRET}`, + { + method: 'POST', + body: JSON.stringify({ + client_id: await this.getOrCreateClientId(), + events: [ + { + name, + params + } + ] + }) + } + ); + if (!this.debug) { + return; + } + console.log(await response.text()); + } catch (e) { + console.error('Google Analytics request failed with an exception', e); + } + } + + // Fire a page view event. + async firePageViewEvent(pageTitle, pageLocation, additionalParams = {}) { + return this.fireEvent('page_view', { + page_title: pageTitle, + page_location: pageLocation, + ...additionalParams + }); + } + + // Fire an error event. + async fireErrorEvent(error, additionalParams = {}) { + // Note: 'error' is a reserved event name and cannot be used + // see https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#reserved_names + return this.fireEvent('extension_error', { + ...error, + ...additionalParams + }); + } +} + +export default new Analytics(); diff --git a/src/service_worker.js b/src/service_worker.js index 04d2e4c..70fd163 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -1,3 +1,5 @@ +import Analytics from './google-analytics.js'; + /** * Handles the extension's installation event. * Sets up context menus. @@ -6,7 +8,7 @@ * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#method-create} * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu} */ -chrome.runtime.onInstalled.addListener(() => { +chrome.runtime.onInstalled.addListener(async (details) => { const menuContexts = ["page"]; const menuRoot = chrome.contextMenus.create({ @@ -42,6 +44,10 @@ chrome.runtime.onInstalled.addListener(() => { id: aboutTheDeveloper.name, title: "About the Developer" }); + + await Analytics.fireEvent('extension_lifecycle', { + reason: details.reason + }); }); /** @@ -60,6 +66,11 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => { }; await handlers[info.menuItemId]?.(tab); + + await Analytics.fireEvent('task_triggered', { + source: 'contextMenu', + task: info.menuItemId + }); }); /** @@ -77,6 +88,11 @@ chrome.commands.onCommand.addListener(async (command, tab) => { }; await handlers[command]?.(tab); + + await Analytics.fireEvent('task_triggered', { + source: 'command', + task: command + }); }); /** From 6c348cc2e28c2d96eef105df3b3e3574c128ad89 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 19 Jul 2024 19:28:13 +1000 Subject: [PATCH 12/18] make contextMenus available on the MV3 unified 'action' icon as well as the legacy 'page' --- src/service_worker.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service_worker.js b/src/service_worker.js index 70fd163..b36352b 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -9,7 +9,7 @@ import Analytics from './google-analytics.js'; * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu} */ chrome.runtime.onInstalled.addListener(async (details) => { - const menuContexts = ["page"]; + const menuContexts = ["page", "action"]; const menuRoot = chrome.contextMenus.create({ contexts: menuContexts, From 9cb049d6a8530bfc2ebadc9645287fb3a59f6827 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Fri, 27 Sep 2024 17:04:05 +1000 Subject: [PATCH 13/18] .gitignore Jetbrains IDE files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index c9d053e..ab62fc0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ dist/ *.zip + +# IDEs +.idea/ From 4650b72ff139cffe514e799a49fdb54a3a18b89b Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Sun, 29 Sep 2024 18:08:48 +1000 Subject: [PATCH 14/18] extract context menu setup into updateContextMenus function --- src/service_worker.js | 83 ++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/src/service_worker.js b/src/service_worker.js index b36352b..cdebdf7 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -5,45 +5,9 @@ import Analytics from './google-analytics.js'; * Sets up context menus. * * @see {@link https://developer.chrome.com/docs/extensions/reference/api/runtime#event-onInstalled} - * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#method-create} - * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu} */ chrome.runtime.onInstalled.addListener(async (details) => { - const menuContexts = ["page", "action"]; - - const menuRoot = chrome.contextMenus.create({ - contexts: menuContexts, - id: 'rootContextMenu', - title: "New window with.." - }); - - chrome.contextMenus.create({ - contexts: menuContexts, - parentId: menuRoot, - id: newWindowWithCurrentAndTabsToRight.name, - title: "..this tab and tabs to right" - }); - - chrome.contextMenus.create({ - contexts: menuContexts, - parentId: menuRoot, - id: newWindowWithTabsToRight.name, - title: "..tabs to right" - }); - - chrome.contextMenus.create({ - contexts: menuContexts, - parentId: menuRoot, - id: 'contextMenu-separator', - type: "separator" - }); - - chrome.contextMenus.create({ - contexts: menuContexts, - parentId: menuRoot, - id: aboutTheDeveloper.name, - title: "About the Developer" - }); + await updateContextMenus(); await Analytics.fireEvent('extension_lifecycle', { reason: details.reason @@ -95,6 +59,51 @@ chrome.commands.onCommand.addListener(async (command, tab) => { }); }); +/** + * Updates the context menus based on the current settings. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#method-removeAll} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#method-create} + * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu} + */ +async function updateContextMenus() { + const menuContexts = ["page", "action"]; + + const menuRoot = chrome.contextMenus.create({ + contexts: menuContexts, + id: 'rootContextMenu', + title: "New window with.." + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: newWindowWithCurrentAndTabsToRight.name, + title: "..this tab and tabs to right" + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: newWindowWithTabsToRight.name, + title: "..tabs to right" + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: 'contextMenu-separator', + type: "separator" + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: aboutTheDeveloper.name, + title: "About the Developer" + }); +} + /** * Opens a new window with the current tab and tabs to the right. * From 2e98a1cbc83d5ce12221161b6527f33866d73e57 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Sun, 29 Sep 2024 18:12:10 +1000 Subject: [PATCH 15/18] improve function header comments + docs references --- src/service_worker.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/service_worker.js b/src/service_worker.js index cdebdf7..b714ff1 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -1,8 +1,8 @@ import Analytics from './google-analytics.js'; /** - * Handles the extension's installation event. - * Sets up context menus. + * Fired when the extension is first installed, when the extension is updated to a new version, and when Chrome is + * updated to a new version. * * @see {@link https://developer.chrome.com/docs/extensions/reference/api/runtime#event-onInstalled} */ @@ -15,12 +15,14 @@ chrome.runtime.onInstalled.addListener(async (details) => { }); /** - * Handles context menu item clicks. + * Fired when a context menu item is clicked. * * @param {object} info - Information about the item clicked and the context where the click happened. * @param {object} tab - The details of the tab where the click happened. * * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#event-onClicked} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/contextMenus#type-OnClickData} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#type-Tab} */ chrome.contextMenus.onClicked.addListener(async (info, tab) => { const handlers = { @@ -38,12 +40,13 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => { }); /** - * Handles keyboard command execution. + * Fired when a registered command is activated using a keyboard shortcut. * * @param {string} command - The name of the command. * @param {object} tab - The details of the tab where the command was executed. * * @see {@link https://developer.chrome.com/docs/extensions/reference/api/commands#event-onCommand} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#type-Tab} */ chrome.commands.onCommand.addListener(async (command, tab) => { const handlers = { @@ -108,6 +111,8 @@ async function updateContextMenus() { * Opens a new window with the current tab and tabs to the right. * * @param {object} tab - The details of the current tab. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#type-Tab} */ async function newWindowWithCurrentAndTabsToRight(tab) { const tabIds = await getTabIdsToMove({ tab, includeCurrentTab: true }); @@ -118,6 +123,8 @@ async function newWindowWithCurrentAndTabsToRight(tab) { * Opens a new window with tabs to the right of the current tab. * * @param {object} tab - The details of the current tab. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#type-Tab} */ async function newWindowWithTabsToRight(tab) { const tabIds = await getTabIdsToMove({ tab, includeCurrentTab: false }); @@ -142,6 +149,7 @@ async function aboutTheDeveloper() { * @returns {Promise} A promise that resolves to an array of tab IDs. * * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#method-query} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#type-Tab} */ async function getTabIdsToMove({ tab, includeCurrentTab }) { let currentTab = tab; From 42e34d00817a96c503cb289b3cb29ef1bd0b680c Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Sun, 29 Sep 2024 18:18:11 +1000 Subject: [PATCH 16/18] add synced settings + contextMenu options section + togglePageContextMenu option --- src/service_worker.js | 76 +++++++++++++++++++++++++++++++++++++++++-- src/settings.js | 36 ++++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 src/settings.js diff --git a/src/service_worker.js b/src/service_worker.js index b714ff1..cce347a 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -1,4 +1,5 @@ import Analytics from './google-analytics.js'; +import { CONTEXT_MENU_SETTINGS_KEYS, STORAGE_KEYS, getSettings, setSettings } from "./settings.js"; /** * Fired when the extension is first installed, when the extension is updated to a new version, and when Chrome is @@ -29,6 +30,7 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => { newWindowWithCurrentAndTabsToRight, newWindowWithTabsToRight, aboutTheDeveloper, + togglePageContextMenu, }; await handlers[info.menuItemId]?.(tab); @@ -62,6 +64,29 @@ chrome.commands.onCommand.addListener(async (command, tab) => { }); }); +/** + * Fired when one or more storage items change. + * + * Handles changes to the extension's settings/etc. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/storage#event-onChanged} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/storage#type-StorageChange} + */ +chrome.storage.onChanged.addListener(async (changes, areaName) => { + if (areaName === 'sync' && STORAGE_KEYS.SETTINGS in changes) { + const { oldValue = {}, newValue = {} } = changes[STORAGE_KEYS.SETTINGS]; + + // Check if any context menu-related settings have changed + const contextMenuSettingsChanged = CONTEXT_MENU_SETTINGS_KEYS.some((key) => { + return oldValue[key] !== newValue[key]; + }); + + if (contextMenuSettingsChanged) { + await updateContextMenus(); + } + } +}); + /** * Updates the context menus based on the current settings. * @@ -70,7 +95,19 @@ chrome.commands.onCommand.addListener(async (command, tab) => { * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/context-menu} */ async function updateContextMenus() { - const menuContexts = ["page", "action"]; + // Get settings from storage + const settings = await getSettings(); + + // Determine the contexts based on settings + const menuContexts = ["action"]; + if (settings.showInPageContext) { + menuContexts.push("page"); + } + + // Remove all existing context menus + await chrome.contextMenus.removeAll(); + + // Create context menus const menuRoot = chrome.contextMenus.create({ contexts: menuContexts, @@ -95,7 +132,32 @@ async function updateContextMenus() { chrome.contextMenus.create({ contexts: menuContexts, parentId: menuRoot, - id: 'contextMenu-separator', + id: 'contextMenu-separator-1', + type: "separator" + }); + + // Create 'Options' submenu + + const optionsMenu = chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: 'optionsMenu', + title: 'Options' + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: optionsMenu, + id: togglePageContextMenu.name, + title: 'Show page context menu', + type: 'checkbox', + checked: settings.showInPageContext, + }); + + chrome.contextMenus.create({ + contexts: menuContexts, + parentId: menuRoot, + id: 'contextMenu-separator-2', type: "separator" }); @@ -140,6 +202,16 @@ async function aboutTheDeveloper() { chrome.tabs.create({ url: "http://devalias.net/dev/chrome-extensions/new-window-with-tabs-to-right/", active: true }); } +/** + * Toggles the option to show the 'page' context menu on or off. + */ +async function togglePageContextMenu() { + await setSettings(settings => ({ + ...settings, + showInPageContext: !settings.showInPageContext, + })); +} + /** * Gets the IDs of tabs to move based on the current tab and whether to include the current tab. * diff --git a/src/settings.js b/src/settings.js new file mode 100644 index 0000000..ab48041 --- /dev/null +++ b/src/settings.js @@ -0,0 +1,36 @@ +export const STORAGE_KEYS = { + SETTINGS: 'settings', +}; + +export const DEFAULT_SETTINGS = { + showInPageContext: true, +}; + +export const CONTEXT_MENU_SETTINGS_KEYS = ['showInPageContext']; + +/** + * Retrieves the extension settings from storage, applying defaults if necessary. + * + * @returns {Promise} A promise that resolves to the settings object. + */ +export async function getSettings() { + const result = await chrome.storage.sync.get(STORAGE_KEYS.SETTINGS); + return { + ...DEFAULT_SETTINGS, + ...result[STORAGE_KEYS.SETTINGS], + }; +} + +/** + * Updates the extension settings in storage using an updater function. + * + * Note: Setting changes are reacted to in the storage change listener + * + * @param {function} updater - A function that receives the current settings and returns the new settings. + * @returns {Promise} + */ +export async function setSettings(updater) { + const currentSettings = await getSettings(); + const newSettings = updater(currentSettings); + await chrome.storage.sync.set({ [STORAGE_KEYS.SETTINGS]: newSettings }); +} From b45a3a01eb1be51b2e882924e5664436706b5c77 Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Sun, 29 Sep 2024 18:21:40 +1000 Subject: [PATCH 17/18] extract/update ABOUT_THE_DEVELOPER_URL --- src/service_worker.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/service_worker.js b/src/service_worker.js index cce347a..015380a 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -1,6 +1,8 @@ import Analytics from './google-analytics.js'; import { CONTEXT_MENU_SETTINGS_KEYS, STORAGE_KEYS, getSettings, setSettings } from "./settings.js"; +const ABOUT_THE_DEVELOPER_URL = 'https://www.devalias.net/dev/chrome-extensions/new-window-with-tabs-to-right/'; + /** * Fired when the extension is first installed, when the extension is updated to a new version, and when Chrome is * updated to a new version. @@ -199,7 +201,7 @@ async function newWindowWithTabsToRight(tab) { * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#method-create} */ async function aboutTheDeveloper() { - chrome.tabs.create({ url: "http://devalias.net/dev/chrome-extensions/new-window-with-tabs-to-right/", active: true }); + await chrome.tabs.create({ url: ABOUT_THE_DEVELOPER_URL, active: true }); } /** From 8275bf3d7a2c5985fd275a99ddfb5edbb7505fdd Mon Sep 17 00:00:00 2001 From: Glenn 'devalias' Grant Date: Sun, 29 Sep 2024 18:32:29 +1000 Subject: [PATCH 18/18] add basic actionIcon click handler calling newWindowWithCurrentAndTabsToRight --- manifest.json | 8 ++++++++ src/service_worker.js | 18 ++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/manifest.json b/manifest.json index 7d39bde..1ae6ee1 100644 --- a/manifest.json +++ b/manifest.json @@ -22,6 +22,14 @@ "service_worker": "src/service_worker.js", "type": "module" }, + "action": { + "default_icon": { + "16": "icons/NewWindowWithTabsToRight-Icon@16px.png", + "32": "icons/NewWindowWithTabsToRight-Icon@32px.png", + "48": "icons/NewWindowWithTabsToRight-Icon@48px.png", + "128": "icons/NewWindowWithTabsToRight-Icon@128px.png" + } + }, "commands": { "newWindowWithCurrentAndTabsToRight": { "suggested_key": { diff --git a/src/service_worker.js b/src/service_worker.js index 015380a..f0b904e 100644 --- a/src/service_worker.js +++ b/src/service_worker.js @@ -17,6 +17,24 @@ chrome.runtime.onInstalled.addListener(async (details) => { }); }); +/** + * Fired when an action icon is clicked. This event will not fire if the action has a popup. + * + * @param {object} tab - The details of the tab where the action button was clicked. + * + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/action#event-onClicked} + * @see {@link https://developer.chrome.com/docs/extensions/reference/api/tabs#type-Tab} + * @see {@link https://developer.chrome.com/docs/extensions/develop/ui/implement-action} + */ +chrome.action.onClicked.addListener(async (tab) => { + await newWindowWithCurrentAndTabsToRight(tab); + + await Analytics.fireEvent('task_triggered', { + source: 'action', + task: 'actionIcon' + }); +}); + /** * Fired when a context menu item is clicked. *