Skip to content

Commit

Permalink
[Feature] Custom domain support (#1)
Browse files Browse the repository at this point in the history
* made each provider a function so it always gives a new object instead of a reference, added name property, added domains property, removed domain property, added canSelfHost property

* getGitProvider now tests only the host of the url instead of the whole string. Removed switch statement for thesting domains in favor of a for loop which will allow custom domains. added new function addGitProvider which adds a copy of the config for given provider and new custom domain

* added custom providers library

* added loading of custom providers

* change async/await calls with with promise.then

* added detect property to provider.selectors object

* added flag isCustom to all custom provider objects for use in filtering

* added mozilla webextension polyfill so storage works across browsers

* wip: custom domains

* added wildcard for content scripts, so that popup can talk to current page

* added eslint exception for parameter reassignment in custom-providers.js

* added isCustom property to each provider, so it is consistent in all provider objects

* Closes #82

* Closes #76

* Added previous selectors as fallback for older instances

* Added ability to opt in to custom websites instead of the extension running on all of them by default. Only chromium browsers supported. Firefox does not allow requesting permissions from background scripts

---------

Co-authored-by: Michael Goodman <bulgedition@gmail.com>
  • Loading branch information
PKief and mikeydevelops committed Jul 4, 2024
1 parent 71ff6c0 commit e153f99
Show file tree
Hide file tree
Showing 24 changed files with 2,466 additions and 603 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
}
},
{
"files": ["src/providers/*.js"],
"files": ["src/providers/*.js", "src/lib/*.js"],
"rules": {
"no-param-reassign": "off"
}
Expand Down
1,772 changes: 1,590 additions & 182 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"npm": "^8.0.0"
},
"dependencies": {
"selector-observer": "2.1.6"
"selector-observer": "2.1.6",
"webextension-polyfill": "0.11.0"
},
"devDependencies": {
"@octokit/core": "3.5.1",
Expand Down
7 changes: 6 additions & 1 deletion scripts/build-src.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ function src(distPath) {
bundleJS(distPath, path.resolve(srcPath, 'ui', 'popup', 'settings-popup.js'));
const bundleOptionsScript = () =>
bundleJS(distPath, path.resolve(srcPath, 'ui', 'options', 'options.js'));
const bundleAll = bundleMainScript().then(bundlePopupScript).then(bundleOptionsScript);
const bundleBackgroundScript = () =>
bundleJS(distPath, path.resolve(srcPath, 'background', 'background.js'));
const bundleAll = bundleMainScript()
.then(bundlePopupScript)
.then(bundleOptionsScript)
.then(bundleBackgroundScript);

const copyPopup = Promise.all(
['settings-popup.html', 'settings-popup.css', 'settings-popup.github-logo.svg'].map((file) =>
Expand Down
35 changes: 35 additions & 0 deletions src/background/background.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Browser from 'webextension-polyfill';

Browser.runtime.onMessage.addListener((message) => {
if (message.event === 'request-access') {
const perm = {
permissions: ['activeTab'],
origins: [`*://${message.data.host}/*`],
};

Browser.permissions.request(perm).then((granted) => {
if (!granted) {
return;
}

// run the script now
Browser.scripting.executeScript({
files: ['./main.js'],
target: {
tabId: message.data.tabId,
},
});

// register content script for future
return Browser.scripting.registerContentScripts([
{
id: 'github-material-icons',
js: ['./main.js'],
css: ['./injected-styles.css'],
matches: [`*://${message.data.host}/*`],
runAt: 'document_start',
},
]);
});
}
});
10 changes: 10 additions & 0 deletions src/lib/custom-providers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Browser from 'webextension-polyfill';

export const getCustomProviders = () =>
Browser.storage.sync.get('customProviders').then((data) => data.customProviders || {});
export const addCustomProvider = (name, handler) =>
getCustomProviders().then((customProviders) => {
customProviders[name] = handler;

return Browser.storage.sync.set({ customProviders });
});
3 changes: 2 additions & 1 deletion src/lib/replace-icon.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Browser from 'webextension-polyfill';
import iconsList from '../icon-list.json';
import iconMap from '../icon-map.json';
import languageMap from '../language-map.json';
Expand Down Expand Up @@ -82,7 +83,7 @@ export function replaceElementWithIcon(iconEl, iconName, fileName, iconPack, pro
newSVG.setAttribute('data-material-icons-extension', 'icon');
newSVG.setAttribute('data-material-icons-extension-iconname', iconName);
newSVG.setAttribute('data-material-icons-extension-filename', fileName);
newSVG.src = chrome.runtime.getURL(`${svgFileName}.svg`);
newSVG.src = Browser.runtime.getURL(`${svgFileName}.svg`);

provider.replaceIcon(iconEl, newSVG);
}
Expand Down
29 changes: 14 additions & 15 deletions src/lib/userConfig.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,34 @@
import Browser from 'webextension-polyfill';

const hardDefaults = {
iconPack: 'react',
iconSize: 'md',
extEnabled: true,
};

export const getConfig = (config, domain = window.location.hostname, useDefault = true) =>
new Promise((resolve) => {
chrome.storage.sync.get(
{
// get custom domain config (if not getting default).
[`${domain !== 'default' ? domain : 'SKIP'}:${config}`]: null,
// also get user default as fallback
[`default:${config}`]: hardDefaults[config],
},
Browser.storage.sync
.get({
// get custom domain config (if not getting default).
[`${domain !== 'default' ? domain : 'SKIP'}:${config}`]: null,
// also get user default as fallback
[`default:${config}`]: hardDefaults[config],
})
.then(
({ [`${domain}:${config}`]: value, [`default:${config}`]: fallback }) =>
resolve(value ?? (useDefault ? fallback : null))
value ?? (useDefault ? fallback : null)
);
});

export const setConfig = (config, value, domain = window.location.hostname) =>
chrome.storage.sync.set({
Browser.storage.sync.set({
[`${domain}:${config}`]: value,
});

export const clearConfig = (config, domain = window.location.hostname) =>
new Promise((resolve) => {
chrome.storage.sync.remove(`${domain}:${config}`, resolve);
});
Browser.storage.sync.remove(`${domain}:${config}`);

export const onConfigChange = (config, handler, domain = window.location.hostname) =>
chrome.storage.onChanged.addListener(
Browser.storage.onChanged.addListener(
(changes) =>
changes[`${domain}:${config}`]?.newValue !== undefined &&
handler(changes[`${domain}:${config}`]?.newValue)
Expand Down
55 changes: 43 additions & 12 deletions src/main.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,49 @@
import Browser from 'webextension-polyfill';
import { getGitProvider } from './providers';
import { observePage, replaceAllIcons } from './lib/replace-icons';
import { initIconSizes } from './lib/icon-sizes';
import { getConfig, onConfigChange } from './lib/userConfig';

initIconSizes();
const { href } = window.location;
const gitProvider = getGitProvider(href);

Promise.all([
getConfig('iconPack'),
getConfig('extEnabled'),
getConfig('extEnabled', 'default'),
]).then(([iconPack, extEnabled, globalExtEnabled]) => {
if (!globalExtEnabled || !extEnabled || !gitProvider) return;
observePage(gitProvider, iconPack);
onConfigChange('iconPack', (newIconPack) => replaceAllIcons(gitProvider, newIconPack));
function init() {
initIconSizes();

const { href } = window.location;

getGitProvider(href).then((gitProvider) => {
Promise.all([
getConfig('iconPack'),
getConfig('extEnabled'),
getConfig('extEnabled', 'default'),
]).then(([iconPack, extEnabled, globalExtEnabled]) => {
if (!globalExtEnabled || !extEnabled || !gitProvider) return;
observePage(gitProvider, iconPack);
onConfigChange('iconPack', (newIconPack) => replaceAllIcons(gitProvider, newIconPack));
});
});
}

const handlers = {
init,

guessProvider(possibilities) {
for (const [name, selector] of Object.entries(possibilities)) {
if (document.querySelector(selector)) {
return name;
}
}

return null;
},
};

Browser.runtime.onMessage.addListener((message, sender, response) => {
if (!handlers[message.cmd]) {
return response(null);
}

const result = handlers[message.cmd].apply(null, message.args || []);

return response(result);
});

init();
2 changes: 1 addition & 1 deletion src/manifests/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,5 @@
"page": "options.html",
"open_in_tab": true
},
"permissions": ["storage", "activeTab"]
"permissions": ["storage", "activeTab", "scripting"]
}
9 changes: 7 additions & 2 deletions src/manifests/chrome-edge.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"*://gitea.com/*",
"*://gitlab.com/*",
"*://gitee.com/*",
"*://sourceforge.net/*"
"*://sourceforge.net/*",
"*://*/*"
]
}
],
Expand All @@ -24,5 +25,9 @@
"48": "icon-48.png",
"128": "icon-128.png"
}
}
},
"background": {
"service_worker": "./background.js"
},
"optional_host_permissions": ["*://*/*"]
}
20 changes: 19 additions & 1 deletion src/manifests/firefox.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,23 @@
"48": "icon-48.png",
"128": "icon-128.png"
}
}
},
"content_scripts": [
{
"matches": [
"*://github.com/*",
"*://bitbucket.org/*",
"*://dev.azure.com/*",
"*://*.visualstudio.com/*",
"*://gitea.com/*",
"*://gitlab.com/*",
"*://gitee.com/*",
"*://sourceforge.net/*",
"*://*/*"
],
"js": ["./main.js"],
"css": ["./injected-styles.css"],
"run_at": "document_start"
}
]
}
113 changes: 64 additions & 49 deletions src/providers/azure.js
Original file line number Diff line number Diff line change
@@ -1,57 +1,72 @@
/** The name of the class used to hide the pseudo element `:before` on Azure */
const HIDE_PSEUDO_CLASS = 'material-icons-exension-hide-pseudo';

const azureConfig = {
domain: 'dev.azure.com',
selectors: {
row: 'table.bolt-table tbody > a',
filename: 'table.bolt-table tbody > a > td[aria-colindex="1"] span.text-ellipsis',
icon: 'td[aria-colindex="1"] span.icon-margin',
},
getIsLightTheme: () =>
document.defaultView.getComputedStyle(document.body).getPropertyValue('color') ===
'rgba(0, 0, 0, 0.9)', // TODO: There is probably a better way to determine whether Azure is in light mode
getIsDirectory: ({ icon }) => icon.classList.contains('repos-folder-icon'),
getIsSubmodule: () => false, // There appears to be no way to tell if a folder is a submodule
getIsSymlink: ({ icon }) => icon.classList.contains('ms-Icon--PageArrowRight'),
replaceIcon: (svgEl, newSVG) => {
newSVG.style.display = 'inline-flex';
newSVG.style.height = '1rem';
newSVG.style.width = '1rem';
export default function azure() {
return {
name: 'azure',
domains: [
{
host: 'dev.azure.com',
test: /^dev\.azure\.com$/,
},
{
host: 'visualstudio.com',
test: /.*\.visualstudio\.com$/,
},
],
selectors: {
row: 'table.bolt-table tbody tr.bolt-table-row, table.bolt-table tbody > a',
filename:
'td.bolt-table-cell[data-column-index="0"] .bolt-table-link .text-ellipsis, table.bolt-table tbody > a > td[aria-colindex="1"] span.text-ellipsis',
icon: 'td.bolt-table-cell[data-column-index="0"] span.icon-margin, td[aria-colindex="1"] span.icon-margin',
// Element by which to detect if the tested domain is azure.
detect: 'body > input[type=hidden][name=__RequestVerificationToken]',
},
canSelfHost: false,
isCustom: false,
getIsLightTheme: () =>
document.defaultView.getComputedStyle(document.body).getPropertyValue('color') ===
'rgba(0, 0, 0, 0.9)', // TODO: There is probably a better way to determine whether Azure is in light mode
getIsDirectory: ({ icon }) => icon.classList.contains('repos-folder-icon'),
getIsSubmodule: () => false, // There appears to be no way to tell if a folder is a submodule
getIsSymlink: ({ icon }) => icon.classList.contains('ms-Icon--PageArrowRight'),
replaceIcon: (svgEl, newSVG) => {
newSVG.style.display = 'inline-flex';
newSVG.style.height = '1rem';
newSVG.style.width = '1rem';

if (!svgEl.classList.contains(HIDE_PSEUDO_CLASS)) {
svgEl.classList.add(HIDE_PSEUDO_CLASS);
}

// Instead of replacing the child icon, add the new icon as a child,
// otherwise Azure DevOps crashes when you navigate through the repository
if (svgEl.hasChildNodes()) {
svgEl.replaceChild(newSVG, svgEl.firstChild);
} else {
svgEl.appendChild(newSVG);
}
},
onAdd: (row, callback) => {
// Mutation observer is required for azure to work properly because the rows are not removed
// from the page when navigating through the repository. Without this the page will render
// fine initially but any subsequent changes will reult in inaccurate icons.
const mutationCallback = (mutationsList) => {
// Check whether the mutation was made by this extension
// this is determined by whether there is an image node added to the dom
const isExtensionMutation = mutationsList.some((mutation) =>
Array.from(mutation.addedNodes).some((node) => node.nodeName === 'IMG')
);
if (!svgEl.classList.contains(HIDE_PSEUDO_CLASS)) {
svgEl.classList.add(HIDE_PSEUDO_CLASS);
}

// If the mutation was not caused by the extension, run the icon replacement
// otherwise there will be an infinite loop
if (!isExtensionMutation) {
callback();
// Instead of replacing the child icon, add the new icon as a child,
// otherwise Azure DevOps crashes when you navigate through the repository
if (svgEl.hasChildNodes()) {
svgEl.replaceChild(newSVG, svgEl.firstChild);
} else {
svgEl.appendChild(newSVG);
}
};
},
onAdd: (row, callback) => {
// Mutation observer is required for azure to work properly because the rows are not removed
// from the page when navigating through the repository. Without this the page will render
// fine initially but any subsequent changes will reult in inaccurate icons.
const mutationCallback = (mutationsList) => {
// Check whether the mutation was made by this extension
// this is determined by whether there is an image node added to the dom
const isExtensionMutation = mutationsList.some((mutation) =>
Array.from(mutation.addedNodes).some((node) => node.nodeName === 'IMG')
);

const observer = new MutationObserver(mutationCallback);
observer.observe(row, { attributes: true, childList: true, subtree: true });
},
};
// If the mutation was not caused by the extension, run the icon replacement
// otherwise there will be an infinite loop
if (!isExtensionMutation) {
callback();
}
};

export default azureConfig;
const observer = new MutationObserver(mutationCallback);
observer.observe(row, { attributes: true, childList: true, subtree: true });
},
};
}
Loading

0 comments on commit e153f99

Please sign in to comment.