refactoring

This commit is contained in:
claudiohbsantos 2022-07-17 00:12:03 -04:00
parent da3a5a55cd
commit e144b442d7
14 changed files with 293 additions and 306 deletions

View File

@ -14,7 +14,8 @@
},
"rules": {
"no-use-before-define": ["error", { "functions": false }],
"jsdoc/require-jsdoc": "off"
"jsdoc/require-jsdoc": "off",
"import/prefer-default-export": "off"
},
"overrides": [
{

View File

@ -34,7 +34,7 @@ function src(distPath) {
const copyPopup = Promise.all(
['html', 'js', 'css'].map((ext) =>
fs.copy(
path.resolve(srcPath, `settings-popup.${ext}`),
path.resolve(srcPath, 'ui', 'popup', `settings-popup.${ext}`),
path.resolve(distPath, `settings-popup.${ext}`)
)
)
@ -45,7 +45,7 @@ function src(distPath) {
path.resolve(distPath, 'injected-styles.css')
);
const copyExtensionLogos = fs.copy(path.resolve(srcPath, 'icons'), distPath);
const copyExtensionLogos = fs.copy(path.resolve(srcPath, 'extensionIcons'), distPath);
return Promise.all([copyExtensionLogos, copyPopup, copyStyles, bundleMainScript, copyIcons]);
}

View File

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 730 B

View File

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

20
src/lib/icon-sizes.js Normal file
View File

@ -0,0 +1,20 @@
const getSizeConfig = () =>
chrome.storage.sync
.get({
iconSize: 'md',
})
.then(({ iconSize }) => iconSize);
const setSizeAttribute = (iconSize) =>
document.body.setAttribute(`data-material-icons-extension-size`, iconSize);
export const initIconSizes = () => {
const setIconSize = () => getSizeConfig().then(setSizeAttribute);
document.addEventListener('DOMContentLoaded', setIconSize, false);
chrome.storage.onChanged.addListener((changes) => {
const newIconSize = changes.iconSize?.newValue;
if (newIconSize) document.body.setAttribute(`data-material-icons-extension-size`, newIconSize);
});
};

194
src/lib/replace-icon.js Normal file
View File

@ -0,0 +1,194 @@
import iconsList from '../icon-list.json';
import iconMap from '../icon-map.json';
import languageMap from '../language-map.json';
/**
* Replace file/folder icons.
*
* @param {HTMLElement} itemRow Item Row.
* @param {object} provider Git Provider specs.
* @returns {undefined}
*/
export function replaceIcon(itemRow, provider) {
const isLightTheme = provider.getIsLightTheme();
// Get file/folder name.
const fileName = itemRow
.querySelector(provider.selectors.filename)
?.innerText?.split('/')[0] // get first part of path for a proper icon lookup
.trim();
if (!fileName) return; // fileName couldn't be found or we don't have a match for it.
// Get file extension.
const fileExtension = fileName.match(/.*?[.](?<ext>xml.dist|xml.dist.sample|yml.dist|\w+)$/)?.[1];
// SVG to be replaced.
const svgEl = itemRow.querySelector(provider.selectors.icon);
if (!svgEl) return; // couldn't find svg element.
// Get Directory or Submodule type.
const isDir = provider.getIsDirectory({ row: itemRow, icon: svgEl });
const isSubmodule = provider.getIsSubmodule({ row: itemRow, icon: svgEl });
const isSymlink = provider.getIsSymlink({ row: itemRow, icon: svgEl });
const lowerFileName = fileName.toLowerCase();
// Get icon name.
let iconName = lookForMatch(
fileName,
lowerFileName,
fileExtension,
isDir,
isSubmodule,
isSymlink
); // returns icon name if found or undefined.
if (isLightTheme) {
iconName = lookForLightMatch(iconName, fileName, fileExtension, isDir); // returns icon name if found for light mode or undefined.
}
// Get folder icon from active icon pack.
if (iconMap.options.activeIconPack) {
iconName = lookForIconPackMatch(lowerFileName) ?? iconName;
}
if (!iconName) return;
const newSVG = document.createElement('img');
newSVG.setAttribute('data-material-icons-extension', 'icon');
newSVG.src = chrome.runtime.getURL(`${iconName}.svg`);
provider.replaceIcon(svgEl, newSVG);
}
/**
* Lookup for matched file/folder icon name.
*
* @since 1.0.0
* @param {string} fileName File name.
* @param {string} lowerFileName Lowercase file name.
* @param {string} fileExtension File extension.
* @param {boolean} isDir Check if directory type.
* @param {boolean} isSubmodule Check if submodule type.
* @param {boolean} isSymlink Check if symlink
* @returns {string} The matched icon name.
*/
function lookForMatch(fileName, lowerFileName, fileExtension, isDir, isSubmodule, isSymlink) {
if (isSubmodule) return 'folder-git';
if (isSymlink) return 'folder-symlink';
// First look in fileNames and folderNames.
if (iconMap.fileNames[fileName] && !isDir && !isSubmodule) return iconMap.fileNames[fileName];
if (iconMap.folderNames[fileName] && isDir && !isSubmodule) return iconMap.folderNames[fileName];
// Then check all lowercase.
if (iconMap.fileNames[lowerFileName] && !isDir && !isSubmodule)
return iconMap.fileNames[lowerFileName];
if (iconMap.folderNames[lowerFileName] && isDir && !isSubmodule)
return iconMap.folderNames[lowerFileName];
// Look for extension in fileExtensions and languageIds.
if (iconMap.fileExtensions[fileExtension] && !isDir && !isSubmodule)
return iconMap.fileExtensions[fileExtension];
if (iconMap.languageIds[fileExtension] && !isDir && !isSubmodule)
return iconMap.languageIds[fileExtension];
// Look for filename and extension in VSCode language map.
if (languageMap.fileNames[fileName] && !isDir && !isSubmodule)
return languageMap.fileNames[fileName];
if (languageMap.fileNames[lowerFileName] && !isDir && !isSubmodule)
return languageMap.fileNames[lowerFileName];
if (languageMap.fileExtensions[fileExtension] && !isDir)
return languageMap.fileExtensions[fileExtension];
// Fallback into default file or folder if no matches.
if (isDir) return 'folder';
return 'file';
}
/**
* Lookup for matched light file/folder icon name.
*
* @since 1.4.0
* @param {string} iconName Icon name.
* @param {string} fileName File name.
* @param {string} fileExtension File extension.
* @param {boolean} isDir Check if directory or file type.
* @returns {string} The matched icon name.
*/
function lookForLightMatch(iconName, fileName, fileExtension, isDir) {
// First look in fileNames and folderNames.
if (iconMap.light.fileNames[fileName] && !isDir) return iconMap.light.fileNames[fileName];
if (iconMap.light.folderNames[fileName] && isDir) return iconMap.light.folderNames[fileName];
// Look for extension in fileExtensions and languageIds.
if (iconMap.light.fileExtensions[fileExtension] && !isDir)
return iconMap.light.fileExtensions[fileExtension];
return iconName;
}
/**
* Lookup for matched icon from active icon pack.
*
* @since 1.4.0
* @param {string} lowerFileName Lowercase file name.
* @returns {string | null} The matched icon name.
*/
function lookForIconPackMatch(lowerFileName) {
if (iconMap.options.activeIconPack) {
switch (iconMap.options.activeIconPack) {
case 'angular':
case 'angular_ngrx':
if (iconsList[`folder-react-${lowerFileName}.svg`]) return `folder-ngrx-${lowerFileName}`;
break;
case 'react':
case 'react_redux':
if (iconsList[`folder-react-${lowerFileName}.svg`]) {
return `folder-react-${lowerFileName}`;
}
if (iconsList[`folder-redux-${lowerFileName}.svg`]) {
return `folder-redux-${lowerFileName}`;
}
break;
case 'vue':
case 'vue_vuex':
if (iconsList[`folder-vuex-${lowerFileName}.svg`]) {
return `folder-vuex-${lowerFileName}`;
}
if (iconsList[`folder-vue-${lowerFileName}.svg`]) {
return `folder-vue-${lowerFileName}`;
}
if (lowerFileName === 'nuxt') {
return `folder-nuxt`;
}
break;
case 'nest':
switch (true) {
case /\.controller\.(t|j)s$/.test(lowerFileName):
return `nest-controller`;
case /\.middleware\.(t|j)s$/.test(lowerFileName):
return 'nest-middleware';
case /\.module\.(t|j)s$/.test(lowerFileName):
return 'nest-module';
case /\.service\.(t|j)s$/.test(lowerFileName):
return 'nest-service';
case /\.decorator\.(t|j)s$/.test(lowerFileName):
return 'nest-decorator';
case /\.pipe\.(t|j)s$/.test(lowerFileName):
return 'nest-pipe';
case /\.filter\.(t|j)s$/.test(lowerFileName):
return 'nest-filter';
case /\.gateway\.(t|j)s$/.test(lowerFileName):
return 'nest-gateway';
case /\.guard\.(t|j)s$/.test(lowerFileName):
return 'nest-guard';
case /\.resolver\.(t|j)s$/.test(lowerFileName):
return 'nest-resolver';
default:
return null;
}
default:
return null;
}
}
return null;
}

34
src/lib/replace-icons.js Normal file
View File

@ -0,0 +1,34 @@
import { observe } from 'selector-observer';
import { replaceIcon } from './replace-icon';
// replacing all icons synchronously prevents visual "blinks" but can
// cause missing icons/rendering delay in very large folders
// replacing asynchronously instead fixes problems in large folders, but introduces "blinks"
// Here we compromise, rushing the first n replacements to prevent blinks that will likely be "above the fold"
// and delaying the replacement of subsequent rows
let executions = 0;
let timerID;
const rushFirst = (rushBatch, callback) => {
if (executions <= rushBatch) {
callback(); // immediately run to prevent visual "blink"
setTimeout(callback, 20); // run again later to catch any icons that are missed in large repositories
executions += 1;
} else {
setTimeout(callback, 0); // run without blocking to prevent delayed rendering of large folders too much
clearTimeout(timerID);
timerID = setTimeout(() => {
executions = 0;
}, 1000); // reset execution tracker
}
};
// Monitor DOM elements that match a CSS selector.
export const observePage = (gitProvider) =>
observe(gitProvider.selectors.row, {
add(row) {
const callback = () => replaceIcon(row, gitProvider);
rushFirst(90, callback);
gitProvider.onAdd(row, callback);
},
});

View File

@ -1,15 +1,8 @@
/**
* External depedencies.
*/
import { observe } from 'selector-observer';
/**
* Internal depedencies.
*/
import iconsList from './icon-list.json';
import iconMap from './icon-map.json';
import languageMap from './language-map.json';
import providerConfig from './providers';
import { getGitProvider } from './providers';
import { observePage } from './lib/replace-icons';
import { initIconSizes } from './lib/icon-sizes';
// Expected configuration.
iconMap.options = {
@ -18,296 +11,7 @@ iconMap.options = {
// activeIconPack: 'nest', // TODO: implement interface to choose pack
};
// replacing all icons synchronously prevents visual "blinks" but can
// cause missing icons/rendering delay in very large folders
// replacing asynchronously instead fixes problems in large folders, but introduces "blinks"
// Here we compromise, rushing the first n replacements to prevent blinks that will likely be "above the fold"
// and delaying the replacement of subsequent rows
let executions = 0;
let timerID;
const rushFirst = (rushBatch, callback) => {
if (executions <= rushBatch) {
callback(); // immediately run to prevent visual "blink"
setTimeout(callback, 20); // run again later to catch any icons that are missed in large repositories
executions += 1;
} else {
setTimeout(callback, 0); // run without blocking to prevent delayed rendering of large folders too much
clearTimeout(timerID);
timerID = setTimeout(() => {
executions = 0;
}, 1000); // reset execution tracker
}
};
/**
* Get all selectors and functions specific to the Git provider
*
* @returns {object} All of the values needed for the provider
*/
const getGitProvider = () => {
const { href } = window.location;
switch (true) {
case /github\.com.*/.test(href):
return providerConfig.github;
case /bitbucket\.org/.test(href):
return providerConfig.bitbucket;
case /gitea\.com/.test(href):
return providerConfig.gitea;
case /dev\.azure\.com/.test(href):
case /visualstudio\.com/.test(href):
return providerConfig.azure;
case /gitlab\.com/.test(href):
return providerConfig.gitlab;
case /gitee\.com/.test(href):
return providerConfig.gitee;
case /sourceforge\.net/.test(href):
return providerConfig.sourceforge;
default:
return null;
}
};
initIconSizes();
const gitProvider = getGitProvider();
let iconSize = 'md';
let hasAddedSizeRules = false;
const init = () => {
// Monitor DOM elements that match a CSS selector.
if (gitProvider) {
observe(gitProvider.selectors.row, {
add(row) {
if (!hasAddedSizeRules) {
// Add the icon size attribute to the body
// This needs to be done here because the DOM is not always ready immediately
document.body.setAttribute(`data-material-icons-extension-size`, iconSize);
// Only add this once
hasAddedSizeRules = true;
}
const callback = () => replaceIcon(row, gitProvider);
rushFirst(90, callback);
gitProvider.onAdd(row, callback);
},
});
}
};
chrome.storage.sync.get(
{
iconSize: 'md',
},
(result) => {
iconSize = result.iconSize;
init();
}
);
chrome.storage.onChanged.addListener((changes) => {
const newIconSize = changes.iconSize?.newValue;
if (newIconSize) {
iconSize = newIconSize;
document.body.setAttribute(`data-material-icons-extension-size`, iconSize);
}
});
/**
* Replace file/folder icons.
*
* @param {HTMLElement} itemRow Item Row.
* @param {object} provider Git Provider specs.
* @returns {undefined}
*/
function replaceIcon(itemRow, provider) {
const isLightTheme = provider.getIsLightTheme();
// Get file/folder name.
const fileName = itemRow
.querySelector(provider.selectors.filename)
?.innerText?.split('/')[0] // get first part of path for a proper icon lookup
.trim();
if (!fileName) return; // fileName couldn't be found or we don't have a match for it.
// Get file extension.
const fileExtension = fileName.match(/.*?[.](?<ext>xml.dist|xml.dist.sample|yml.dist|\w+)$/)?.[1];
// SVG to be replaced.
const svgEl = itemRow.querySelector(provider.selectors.icon);
if (!svgEl) return; // couldn't find svg element.
// Get Directory or Submodule type.
const isDir = provider.getIsDirectory({ row: itemRow, icon: svgEl });
const isSubmodule = provider.getIsSubmodule({ row: itemRow, icon: svgEl });
const isSymlink = provider.getIsSymlink({ row: itemRow, icon: svgEl });
const lowerFileName = fileName.toLowerCase();
// Get icon name.
let iconName = lookForMatch(
fileName,
lowerFileName,
fileExtension,
isDir,
isSubmodule,
isSymlink
); // returns icon name if found or undefined.
if (isLightTheme) {
iconName = lookForLightMatch(iconName, fileName, fileExtension, isDir); // returns icon name if found for light mode or undefined.
}
// Get folder icon from active icon pack.
if (iconMap.options.activeIconPack) {
iconName = lookForIconPackMatch(lowerFileName) ?? iconName;
}
if (!iconName) return;
const newSVG = document.createElement('img');
newSVG.setAttribute('data-material-icons-extension', 'icon');
newSVG.src = chrome.runtime.getURL(`${iconName}.svg`);
provider.replaceIcon(svgEl, newSVG);
}
/**
* Lookup for matched file/folder icon name.
*
* @since 1.0.0
* @param {string} fileName File name.
* @param {string} lowerFileName Lowercase file name.
* @param {string} fileExtension File extension.
* @param {boolean} isDir Check if directory type.
* @param {boolean} isSubmodule Check if submodule type.
* @param {boolean} isSymlink Check if symlink
* @returns {string} The matched icon name.
*/
function lookForMatch(fileName, lowerFileName, fileExtension, isDir, isSubmodule, isSymlink) {
if (isSubmodule) return 'folder-git';
if (isSymlink) return 'folder-symlink';
// First look in fileNames and folderNames.
if (iconMap.fileNames[fileName] && !isDir && !isSubmodule) return iconMap.fileNames[fileName];
if (iconMap.folderNames[fileName] && isDir && !isSubmodule) return iconMap.folderNames[fileName];
// Then check all lowercase.
if (iconMap.fileNames[lowerFileName] && !isDir && !isSubmodule)
return iconMap.fileNames[lowerFileName];
if (iconMap.folderNames[lowerFileName] && isDir && !isSubmodule)
return iconMap.folderNames[lowerFileName];
// Look for extension in fileExtensions and languageIds.
if (iconMap.fileExtensions[fileExtension] && !isDir && !isSubmodule)
return iconMap.fileExtensions[fileExtension];
if (iconMap.languageIds[fileExtension] && !isDir && !isSubmodule)
return iconMap.languageIds[fileExtension];
// Look for filename and extension in VSCode language map.
if (languageMap.fileNames[fileName] && !isDir && !isSubmodule)
return languageMap.fileNames[fileName];
if (languageMap.fileNames[lowerFileName] && !isDir && !isSubmodule)
return languageMap.fileNames[lowerFileName];
if (languageMap.fileExtensions[fileExtension] && !isDir)
return languageMap.fileExtensions[fileExtension];
// Fallback into default file or folder if no matches.
if (isDir) return 'folder';
return 'file';
}
/**
* Lookup for matched light file/folder icon name.
*
* @since 1.4.0
* @param {string} iconName Icon name.
* @param {string} fileName File name.
* @param {string} fileExtension File extension.
* @param {boolean} isDir Check if directory or file type.
* @returns {string} The matched icon name.
*/
function lookForLightMatch(iconName, fileName, fileExtension, isDir) {
// First look in fileNames and folderNames.
if (iconMap.light.fileNames[fileName] && !isDir) return iconMap.light.fileNames[fileName];
if (iconMap.light.folderNames[fileName] && isDir) return iconMap.light.folderNames[fileName];
// Look for extension in fileExtensions and languageIds.
if (iconMap.light.fileExtensions[fileExtension] && !isDir)
return iconMap.light.fileExtensions[fileExtension];
return iconName;
}
/**
* Lookup for matched icon from active icon pack.
*
* @since 1.4.0
* @param {string} lowerFileName Lowercase file name.
* @returns {string} The matched icon name.
*/
function lookForIconPackMatch(lowerFileName) {
if (iconMap.options.activeIconPack) {
switch (iconMap.options.activeIconPack) {
case 'angular':
case 'angular_ngrx':
if (iconsList[`folder-react-${lowerFileName}.svg`]) return `folder-ngrx-${lowerFileName}`;
break;
case 'react':
case 'react_redux':
if (iconsList[`folder-react-${lowerFileName}.svg`]) {
return `folder-react-${lowerFileName}`;
}
if (iconsList[`folder-redux-${lowerFileName}.svg`]) {
return `folder-redux-${lowerFileName}`;
}
break;
case 'vue':
case 'vue_vuex':
if (iconsList[`folder-vuex-${lowerFileName}.svg`]) {
return `folder-vuex-${lowerFileName}`;
}
if (iconsList[`folder-vue-${lowerFileName}.svg`]) {
return `folder-vue-${lowerFileName}`;
}
if (lowerFileName === 'nuxt') {
return `folder-nuxt`;
}
break;
case 'nest':
switch (true) {
case /\.controller\.(t|j)s$/.test(lowerFileName):
return `nest-controller`;
case /\.middleware\.(t|j)s$/.test(lowerFileName):
return 'nest-middleware';
case /\.module\.(t|j)s$/.test(lowerFileName):
return 'nest-module';
case /\.service\.(t|j)s$/.test(lowerFileName):
return 'nest-service';
case /\.decorator\.(t|j)s$/.test(lowerFileName):
return 'nest-decorator';
case /\.pipe\.(t|j)s$/.test(lowerFileName):
return 'nest-pipe';
case /\.filter\.(t|j)s$/.test(lowerFileName):
return 'nest-filter';
case /\.gateway\.(t|j)s$/.test(lowerFileName):
return 'nest-gateway';
case /\.guard\.(t|j)s$/.test(lowerFileName):
return 'nest-guard';
case /\.resolver\.(t|j)s$/.test(lowerFileName):
return 'nest-resolver';
default:
return null;
}
default:
return null;
}
}
return null;
}
if (gitProvider) observePage(gitProvider);

View File

@ -16,4 +16,38 @@ const providerConfig = {
sourceforge: sourceforgeConfig,
};
export default providerConfig;
/**
* Get all selectors and functions specific to the Git provider
*
* @returns {object} All of the values needed for the provider
*/
export const getGitProvider = () => {
const { href } = window.location;
switch (true) {
case /github\.com.*/.test(href):
return providerConfig.github;
case /bitbucket\.org/.test(href):
return providerConfig.bitbucket;
case /gitea\.com/.test(href):
return providerConfig.gitea;
case /dev\.azure\.com/.test(href):
case /visualstudio\.com/.test(href):
return providerConfig.azure;
case /gitlab\.com/.test(href):
return providerConfig.gitlab;
case /gitee\.com/.test(href):
return providerConfig.gitee;
case /sourceforge\.net/.test(href):
return providerConfig.sourceforge;
default:
return null;
}
};