From 68955543100bf4562b2cfb317e247e3520d3a0c3 Mon Sep 17 00:00:00 2001 From: Charly Koch Date: Fri, 17 Oct 2025 19:39:30 +0200 Subject: [PATCH 1/2] feat: add options with custom hosts --- README.md | 1 + background.js | 44 +++++++++++++++++++++++ manifest.json | 18 ++++++++-- options.html | 30 ++++++++++++++++ options.js | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 background.js create mode 100644 options.html create mode 100644 options.js diff --git a/README.md b/README.md index 1ece64e..b2405d9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ A browser extension that brings syntax highlighting to file diffs in Azure DevOp - **Language Detection:** Detects the programming language based on file extensions. - **Theme Support:** Seamlessly integrates with both light and dark themes in Azure DevOps. - **Powered by Prism:** Utilizes the popular [Prism](https://prismjs.com/) library for fast and accurate highlighting. +- **Custom Domains Support**: Works with self-hosted (on-premise) and other custom Azure DevOps domains via a simple configuration page. ## Screenshots diff --git a/background.js b/background.js new file mode 100644 index 0000000..ebcc6b7 --- /dev/null +++ b/background.js @@ -0,0 +1,44 @@ +function injectContent(tabId) { + console.log(`ADO Syntax Highlighter: Injecting into custom host on tab ${tabId}`); + chrome.scripting.insertCSS({ + target: { tabId: tabId }, + files: ["prism/prism.css", "custom_styles.css"], + }).catch(err => console.warn(`CSS injection warning: ${err.message}`)); + + chrome.scripting.executeScript({ + target: { tabId: tabId }, + files: ["prism/prism.js", "content_script.js"], + }); +} + +chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (changeInfo.status !== 'complete' || !tab.url || !tab.url.startsWith('http')) { + return; + } + + const manifest = chrome.runtime.getManifest(); + const defaultHosts = manifest.host_permissions || []; + const isDefaultHost = defaultHosts.some(pattern => { + const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); + return regex.test(tab.url); + }); + + if (isDefaultHost) { + return; + } + + const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + if (customHosts.length === 0) { + return; + } + + for (const pattern of customHosts) { + const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); + + if (regex.test(tab.url)) { + console.log(`URL "${tab.url}" matched custom pattern "${pattern}". Injecting scripts.`); + injectContent(tabId); + return; + } + } +}); \ No newline at end of file diff --git a/manifest.json b/manifest.json index 6537189..8ccc8f5 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/chrome-manifest", "manifest_version": 3, "name": "Syntax Highlighter for Azure DevOps", - "version": "0.5.0", + "version": "0.6.0", "description": "Enhances code readability by adding syntax highlighting to code views and diffs within Azure DevOps, including pull requests.", "icons": { "16": "assets/icons/icon16.png", @@ -10,10 +10,24 @@ "48": "assets/icons/icon48.png", "128": "assets/icons/icon128.png" }, + "permissions": [ + "storage", + "scripting" + ], "host_permissions": [ "*://*.dev.azure.com/*", "*://*.visualstudio.com/*" ], + "optional_host_permissions": [ + "*://*/*" + ], + "background": { + "service_worker": "background.js" + }, + "options_ui": { + "page": "options.html", + "open_in_tab": true + }, "content_scripts": [ { "matches": [ @@ -25,4 +39,4 @@ "run_at": "document_idle" } ] -} +} \ No newline at end of file diff --git a/options.html b/options.html new file mode 100644 index 0000000..0574a5d --- /dev/null +++ b/options.html @@ -0,0 +1,30 @@ + + + + Syntax Highlighter for Azure Devops options + + + +

Syntax Highlighter for Azure Devops options

+
+ +

Granted Hosts

+ +

Default Hosts (Always Active)

+ + +

Manage Custom Hosts

+

+ Enter a hostname (devops.mycompany.com) or a specific path (devops.mycompany.com/my-project). The extension will activate on that page and any sub-pages. +

+ + + + + + + + + \ No newline at end of file diff --git a/options.js b/options.js new file mode 100644 index 0000000..2ed87b1 --- /dev/null +++ b/options.js @@ -0,0 +1,98 @@ +const hostInput = document.getElementById('host-input'); +const addHostBtn = document.getElementById('add-host-btn'); +const hostsList = document.getElementById('hosts-list'); +const defaultHostsList = document.getElementById('default-hosts-list'); + +function createCustomHostListItem(host) { + const listItem = document.createElement('li'); + listItem.textContent = host; + + const removeBtn = document.createElement('button'); + removeBtn.textContent = 'Remove'; + removeBtn.style.marginLeft = '10px'; + removeBtn.addEventListener('click', () => removeHost(host)); + + listItem.appendChild(removeBtn); + hostsList.appendChild(listItem); +} + +function loadDefaultHosts() { + const manifest = chrome.runtime.getManifest(); + const defaultHosts = manifest.host_permissions || []; + + defaultHostsList.innerHTML = ''; + defaultHosts.forEach(host => { + const listItem = document.createElement('li'); + listItem.textContent = host; + defaultHostsList.appendChild(listItem); + }); +} + +async function loadCustomHosts() { + const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + hostsList.innerHTML = ''; // Clear the list before populating + customHosts.forEach(createCustomHostListItem); +} + +async function addHost() { + let hostValue = hostInput.value.trim(); + if (!hostValue) return; + + hostValue = hostValue.replace(/\/+$/, ''); + + const permissionPattern = `*://${hostValue}/*`; + + try { + const granted = await chrome.permissions.request({ + origins: [permissionPattern] + }); + + if (granted) { + const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + if (!customHosts.includes(permissionPattern)) { + const updatedHosts = [...customHosts, permissionPattern]; + await chrome.storage.sync.set({ customHosts: updatedHosts }); + createCustomHostListItem(permissionPattern); + } + hostInput.value = ''; + } else { + console.warn('Permission was not granted by the user.'); + } + } catch (err) { + console.error(`Error requesting permission: ${err}`); + + alert(`Could not request permission for "${hostValue}". Please ensure it's a valid hostname.`); + } +} + +async function removeHost(hostToRemove) { + try { + const removed = await chrome.permissions.remove({ + origins: [hostToRemove] + }); + + if (removed) { + const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + const updatedHosts = customHosts.filter(h => h !== hostToRemove); + await chrome.storage.sync.set({ customHosts: updatedHosts }); + loadCustomHosts(); + } else { + console.warn('Could not remove permission.'); + } + } catch (err) { + console.error(`Error removing permission: ${err}`); + } +} + +addHostBtn.addEventListener('click', addHost); + +hostInput.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + addHost(); + } +}); + +document.addEventListener('DOMContentLoaded', () => { + loadDefaultHosts(); + loadCustomHosts(); +}); \ No newline at end of file From c202aa64797f685c837c87cebd8768c99c150654 Mon Sep 17 00:00:00 2001 From: Charly Koch Date: Mon, 20 Oct 2025 20:17:50 +0200 Subject: [PATCH 2/2] feat: use browser polyfill instead of chrome API --- background.js | 14 ++++++++------ browser-polyfill.min.js | 8 ++++++++ manifest.json | 2 +- options.html | 10 ++++++---- options.js | 21 ++++++++++----------- 5 files changed, 33 insertions(+), 22 deletions(-) create mode 100644 browser-polyfill.min.js diff --git a/background.js b/background.js index ebcc6b7..66f6414 100644 --- a/background.js +++ b/background.js @@ -1,22 +1,24 @@ +importScripts('browser-polyfill.min.js'); + function injectContent(tabId) { console.log(`ADO Syntax Highlighter: Injecting into custom host on tab ${tabId}`); - chrome.scripting.insertCSS({ + browser.scripting.insertCSS({ target: { tabId: tabId }, files: ["prism/prism.css", "custom_styles.css"], }).catch(err => console.warn(`CSS injection warning: ${err.message}`)); - chrome.scripting.executeScript({ + browser.scripting.executeScript({ target: { tabId: tabId }, files: ["prism/prism.js", "content_script.js"], }); } -chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { +browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { if (changeInfo.status !== 'complete' || !tab.url || !tab.url.startsWith('http')) { return; } - const manifest = chrome.runtime.getManifest(); + const manifest = browser.runtime.getManifest(); const defaultHosts = manifest.host_permissions || []; const isDefaultHost = defaultHosts.some(pattern => { const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$'); @@ -27,7 +29,7 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { return; } - const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + const { customHosts = [] } = await browser.storage.sync.get('customHosts'); if (customHosts.length === 0) { return; } @@ -41,4 +43,4 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { return; } } -}); \ No newline at end of file +}); diff --git a/browser-polyfill.min.js b/browser-polyfill.min.js new file mode 100644 index 0000000..0758a1e --- /dev/null +++ b/browser-polyfill.min.js @@ -0,0 +1,8 @@ +(function(a,b){if("function"==typeof define&&define.amd)define("webextension-polyfill",["module"],b);else if("undefined"!=typeof exports)b(module);else{var c={exports:{}};b(c),a.browser=c.exports}})("undefined"==typeof globalThis?"undefined"==typeof self?this:self:globalThis,function(a){"use strict";if(!(globalThis.chrome&&globalThis.chrome.runtime&&globalThis.chrome.runtime.id))throw new Error("This script should only be loaded in a browser extension.");if(!(globalThis.browser&&globalThis.browser.runtime&&globalThis.browser.runtime.id)){a.exports=(a=>{const b={alarms:{clear:{minArgs:0,maxArgs:1},clearAll:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getAll:{minArgs:0,maxArgs:0}},bookmarks:{create:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},getChildren:{minArgs:1,maxArgs:1},getRecent:{minArgs:1,maxArgs:1},getSubTree:{minArgs:1,maxArgs:1},getTree:{minArgs:0,maxArgs:0},move:{minArgs:2,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeTree:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}},browserAction:{disable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},enable:{minArgs:0,maxArgs:1,fallbackToNoCallback:!0},getBadgeBackgroundColor:{minArgs:1,maxArgs:1},getBadgeText:{minArgs:1,maxArgs:1},getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},openPopup:{minArgs:0,maxArgs:0},setBadgeBackgroundColor:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setBadgeText:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},browsingData:{remove:{minArgs:2,maxArgs:2},removeCache:{minArgs:1,maxArgs:1},removeCookies:{minArgs:1,maxArgs:1},removeDownloads:{minArgs:1,maxArgs:1},removeFormData:{minArgs:1,maxArgs:1},removeHistory:{minArgs:1,maxArgs:1},removeLocalStorage:{minArgs:1,maxArgs:1},removePasswords:{minArgs:1,maxArgs:1},removePluginData:{minArgs:1,maxArgs:1},settings:{minArgs:0,maxArgs:0}},commands:{getAll:{minArgs:0,maxArgs:0}},contextMenus:{remove:{minArgs:1,maxArgs:1},removeAll:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},cookies:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:1,maxArgs:1},getAllCookieStores:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},devtools:{inspectedWindow:{eval:{minArgs:1,maxArgs:2,singleCallbackArg:!1}},panels:{create:{minArgs:3,maxArgs:3,singleCallbackArg:!0},elements:{createSidebarPane:{minArgs:1,maxArgs:1}}}},downloads:{cancel:{minArgs:1,maxArgs:1},download:{minArgs:1,maxArgs:1},erase:{minArgs:1,maxArgs:1},getFileIcon:{minArgs:1,maxArgs:2},open:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},pause:{minArgs:1,maxArgs:1},removeFile:{minArgs:1,maxArgs:1},resume:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},extension:{isAllowedFileSchemeAccess:{minArgs:0,maxArgs:0},isAllowedIncognitoAccess:{minArgs:0,maxArgs:0}},history:{addUrl:{minArgs:1,maxArgs:1},deleteAll:{minArgs:0,maxArgs:0},deleteRange:{minArgs:1,maxArgs:1},deleteUrl:{minArgs:1,maxArgs:1},getVisits:{minArgs:1,maxArgs:1},search:{minArgs:1,maxArgs:1}},i18n:{detectLanguage:{minArgs:1,maxArgs:1},getAcceptLanguages:{minArgs:0,maxArgs:0}},identity:{launchWebAuthFlow:{minArgs:1,maxArgs:1}},idle:{queryState:{minArgs:1,maxArgs:1}},management:{get:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},getSelf:{minArgs:0,maxArgs:0},setEnabled:{minArgs:2,maxArgs:2},uninstallSelf:{minArgs:0,maxArgs:1}},notifications:{clear:{minArgs:1,maxArgs:1},create:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:0},getPermissionLevel:{minArgs:0,maxArgs:0},update:{minArgs:2,maxArgs:2}},pageAction:{getPopup:{minArgs:1,maxArgs:1},getTitle:{minArgs:1,maxArgs:1},hide:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setIcon:{minArgs:1,maxArgs:1},setPopup:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},setTitle:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0},show:{minArgs:1,maxArgs:1,fallbackToNoCallback:!0}},permissions:{contains:{minArgs:1,maxArgs:1},getAll:{minArgs:0,maxArgs:0},remove:{minArgs:1,maxArgs:1},request:{minArgs:1,maxArgs:1}},runtime:{getBackgroundPage:{minArgs:0,maxArgs:0},getPlatformInfo:{minArgs:0,maxArgs:0},openOptionsPage:{minArgs:0,maxArgs:0},requestUpdateCheck:{minArgs:0,maxArgs:0},sendMessage:{minArgs:1,maxArgs:3},sendNativeMessage:{minArgs:2,maxArgs:2},setUninstallURL:{minArgs:1,maxArgs:1}},sessions:{getDevices:{minArgs:0,maxArgs:1},getRecentlyClosed:{minArgs:0,maxArgs:1},restore:{minArgs:0,maxArgs:1}},storage:{local:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}},managed:{get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1}},sync:{clear:{minArgs:0,maxArgs:0},get:{minArgs:0,maxArgs:1},getBytesInUse:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}}},tabs:{captureVisibleTab:{minArgs:0,maxArgs:2},create:{minArgs:1,maxArgs:1},detectLanguage:{minArgs:0,maxArgs:1},discard:{minArgs:0,maxArgs:1},duplicate:{minArgs:1,maxArgs:1},executeScript:{minArgs:1,maxArgs:2},get:{minArgs:1,maxArgs:1},getCurrent:{minArgs:0,maxArgs:0},getZoom:{minArgs:0,maxArgs:1},getZoomSettings:{minArgs:0,maxArgs:1},goBack:{minArgs:0,maxArgs:1},goForward:{minArgs:0,maxArgs:1},highlight:{minArgs:1,maxArgs:1},insertCSS:{minArgs:1,maxArgs:2},move:{minArgs:2,maxArgs:2},query:{minArgs:1,maxArgs:1},reload:{minArgs:0,maxArgs:2},remove:{minArgs:1,maxArgs:1},removeCSS:{minArgs:1,maxArgs:2},sendMessage:{minArgs:2,maxArgs:3},setZoom:{minArgs:1,maxArgs:2},setZoomSettings:{minArgs:1,maxArgs:2},update:{minArgs:1,maxArgs:2}},topSites:{get:{minArgs:0,maxArgs:0}},webNavigation:{getAllFrames:{minArgs:1,maxArgs:1},getFrame:{minArgs:1,maxArgs:1}},webRequest:{handlerBehaviorChanged:{minArgs:0,maxArgs:0}},windows:{create:{minArgs:0,maxArgs:1},get:{minArgs:1,maxArgs:2},getAll:{minArgs:0,maxArgs:1},getCurrent:{minArgs:0,maxArgs:1},getLastFocused:{minArgs:0,maxArgs:1},remove:{minArgs:1,maxArgs:1},update:{minArgs:2,maxArgs:2}}};if(0===Object.keys(b).length)throw new Error("api-metadata.json has not been included in browser-polyfill");class c extends WeakMap{constructor(a,b=void 0){super(b),this.createItem=a}get(a){return this.has(a)||this.set(a,this.createItem(a)),super.get(a)}}const d=a=>a&&"object"==typeof a&&"function"==typeof a.then,e=(b,c)=>(...d)=>{a.runtime.lastError?b.reject(new Error(a.runtime.lastError.message)):c.singleCallbackArg||1>=d.length&&!1!==c.singleCallbackArg?b.resolve(d[0]):b.resolve(d)},f=a=>1==a?"argument":"arguments",g=(a,b)=>function(c,...d){if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((f,g)=>{if(b.fallbackToNoCallback)try{c[a](...d,e({resolve:f,reject:g},b))}catch(e){console.warn(`${a} API method doesn't seem to support the callback parameter, `+"falling back to call it without a callback: ",e),c[a](...d),b.fallbackToNoCallback=!1,b.noCallback=!0,f()}else b.noCallback?(c[a](...d),f()):c[a](...d,e({resolve:f,reject:g},b))})},h=(a,b,c)=>new Proxy(b,{apply(b,d,e){return c.call(d,a,...e)}});let i=Function.call.bind(Object.prototype.hasOwnProperty);const j=(a,b={},c={})=>{let d=Object.create(null),e=Object.create(a);return new Proxy(e,{has(b,c){return c in a||c in d},get(e,f){if(f in d)return d[f];if(!(f in a))return;let k=a[f];if("function"==typeof k){if("function"==typeof b[f])k=h(a,a[f],b[f]);else if(i(c,f)){let b=g(f,c[f]);k=h(a,a[f],b)}else k=k.bind(a);}else if("object"==typeof k&&null!==k&&(i(b,f)||i(c,f)))k=j(k,b[f],c[f]);else if(i(c,"*"))k=j(k,b[f],c["*"]);else return Object.defineProperty(d,f,{configurable:!0,enumerable:!0,get(){return a[f]},set(b){a[f]=b}}),k;return d[f]=k,k},set(b,c,e){return c in d?d[c]=e:a[c]=e,!0},defineProperty(a,b,c){return Reflect.defineProperty(d,b,c)},deleteProperty(a,b){return Reflect.deleteProperty(d,b)}})},k=a=>({addListener(b,c,...d){b.addListener(a.get(c),...d)},hasListener(b,c){return b.hasListener(a.get(c))},removeListener(b,c){b.removeListener(a.get(c))}}),l=new c(a=>"function"==typeof a?function(b){const c=j(b,{},{getContent:{minArgs:0,maxArgs:0}});a(c)}:a),m=new c(a=>"function"==typeof a?function(b,c,e){let f,g,h=!1,i=new Promise(a=>{f=function(b){h=!0,a(b)}});try{g=a(b,c,f)}catch(a){g=Promise.reject(a)}const j=!0!==g&&d(g);if(!0!==g&&!j&&!h)return!1;const k=a=>{a.then(a=>{e(a)},a=>{let b;b=a&&(a instanceof Error||"string"==typeof a.message)?a.message:"An unexpected error occurred",e({__mozWebExtensionPolyfillReject__:!0,message:b})}).catch(a=>{console.error("Failed to send onMessage rejected reply",a)})};return j?k(g):k(i),!0}:a),n=({reject:b,resolve:c},d)=>{a.runtime.lastError?a.runtime.lastError.message==="The message port closed before a response was received."?c():b(new Error(a.runtime.lastError.message)):d&&d.__mozWebExtensionPolyfillReject__?b(new Error(d.message)):c(d)},o=(a,b,c,...d)=>{if(d.lengthb.maxArgs)throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`);return new Promise((a,b)=>{const e=n.bind(null,{resolve:a,reject:b});d.push(e),c.sendMessage(...d)})},p={devtools:{network:{onRequestFinished:k(l)}},runtime:{onMessage:k(m),onMessageExternal:k(m),sendMessage:o.bind(null,"sendMessage",{minArgs:1,maxArgs:3})},tabs:{sendMessage:o.bind(null,"sendMessage",{minArgs:2,maxArgs:3})}},q={clear:{minArgs:1,maxArgs:1},get:{minArgs:1,maxArgs:1},set:{minArgs:1,maxArgs:1}};return b.privacy={network:{"*":q},services:{"*":q},websites:{"*":q}},j(a,p,b)})(chrome)}else a.exports=globalThis.browser}); +//# sourceMappingURL=browser-polyfill.min.js.map + +// webextension-polyfill v.0.12.0 (https://github.com/mozilla/webextension-polyfill) + +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ diff --git a/manifest.json b/manifest.json index 8ccc8f5..c27c01f 100644 --- a/manifest.json +++ b/manifest.json @@ -39,4 +39,4 @@ "run_at": "document_idle" } ] -} \ No newline at end of file +} diff --git a/options.html b/options.html index 0574a5d..db6d615 100644 --- a/options.html +++ b/options.html @@ -1,11 +1,12 @@ + - Syntax Highlighter for Azure Devops options + Syntax Highlighter for Azure DevOps Options -

Syntax Highlighter for Azure Devops options

+

Syntax Highlighter for Azure DevOps Options


Granted Hosts

@@ -16,14 +17,15 @@

Default Hosts (Always Active)

Manage Custom Hosts

- Enter a hostname (devops.mycompany.com) or a specific path (devops.mycompany.com/my-project). The extension will activate on that page and any sub-pages. + Enter a hostname (devops.mycompany.com) or a specific path (devops.mycompany.com/ado). The extension will activate on that page and any sub-pages.

    -
+ + diff --git a/options.js b/options.js index 2ed87b1..d871fbf 100644 --- a/options.js +++ b/options.js @@ -17,7 +17,7 @@ function createCustomHostListItem(host) { } function loadDefaultHosts() { - const manifest = chrome.runtime.getManifest(); + const manifest = browser.runtime.getManifest(); const defaultHosts = manifest.host_permissions || []; defaultHostsList.innerHTML = ''; @@ -29,8 +29,8 @@ function loadDefaultHosts() { } async function loadCustomHosts() { - const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); - hostsList.innerHTML = ''; // Clear the list before populating + const { customHosts = [] } = await browser.storage.sync.get('customHosts'); + hostsList.innerHTML = ''; customHosts.forEach(createCustomHostListItem); } @@ -43,15 +43,15 @@ async function addHost() { const permissionPattern = `*://${hostValue}/*`; try { - const granted = await chrome.permissions.request({ + const granted = await browser.permissions.request({ origins: [permissionPattern] }); if (granted) { - const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + const { customHosts = [] } = await browser.storage.sync.get('customHosts'); if (!customHosts.includes(permissionPattern)) { const updatedHosts = [...customHosts, permissionPattern]; - await chrome.storage.sync.set({ customHosts: updatedHosts }); + await browser.storage.sync.set({ customHosts: updatedHosts }); createCustomHostListItem(permissionPattern); } hostInput.value = ''; @@ -60,21 +60,20 @@ async function addHost() { } } catch (err) { console.error(`Error requesting permission: ${err}`); - alert(`Could not request permission for "${hostValue}". Please ensure it's a valid hostname.`); } } async function removeHost(hostToRemove) { try { - const removed = await chrome.permissions.remove({ + const removed = await browser.permissions.remove({ origins: [hostToRemove] }); if (removed) { - const { customHosts = [] } = await chrome.storage.sync.get('customHosts'); + const { customHosts = [] } = await browser.storage.sync.get('customHosts'); const updatedHosts = customHosts.filter(h => h !== hostToRemove); - await chrome.storage.sync.set({ customHosts: updatedHosts }); + await browser.storage.sync.set({ customHosts: updatedHosts }); loadCustomHosts(); } else { console.warn('Could not remove permission.'); @@ -95,4 +94,4 @@ hostInput.addEventListener('keypress', (e) => { document.addEventListener('DOMContentLoaded', () => { loadDefaultHosts(); loadCustomHosts(); -}); \ No newline at end of file +});