Update project setup (#2)
* Update custom SVG files * Update linting and formatting * Remove build of external dependency * Format files * Update biome js * Update dependencies * Update project * Update lint staged * Update meta info * Remove SVGO config * Transform JS to TS * Update link to repo in footer * Update release workflow * Update dependencies * Update package json * Update workflow * Transform require to import * Replace node-fetch with native fetch * Replace parcel with ESBuild * Remove flakyness of build language script * Update release workflow for Edge * Update readme
This commit is contained in:
parent
e153f99d10
commit
11b495ab4d
@ -1,2 +0,0 @@
|
||||
dist
|
||||
node_modules
|
@ -1,41 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"globals": {
|
||||
"chrome": "readonly"
|
||||
},
|
||||
"extends": ["airbnb-base", "plugin:jsdoc/recommended", "prettier"],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-use-before-define": ["error", { "functions": false }],
|
||||
"jsdoc/require-jsdoc": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"consistent-return": "off",
|
||||
"no-restricted-syntax": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["scripts/*.js", "svgo.config.js"],
|
||||
"parserOptions": {
|
||||
"sourceType": "script"
|
||||
},
|
||||
"rules": {
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/no-dynamic-require": "off",
|
||||
"no-console": "off"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/providers/*.js", "src/lib/*.js"],
|
||||
"rules": {
|
||||
"no-param-reassign": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
43
.github/workflows/release.yml
vendored
43
.github/workflows/release.yml
vendored
@ -2,25 +2,39 @@ name: Release new version
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
type:
|
||||
description: major|minor|patch - defaults to patch
|
||||
versionChange:
|
||||
type: choice
|
||||
description: Select the version change
|
||||
required: true
|
||||
default: patch
|
||||
default: 'patch'
|
||||
options:
|
||||
- major
|
||||
- minor
|
||||
- patch
|
||||
onlyUpload:
|
||||
description: chrome|firefox|both|not|none - Only upload to store(s) without bumping version or releasing
|
||||
required: true
|
||||
default: not
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16.13.0'
|
||||
node-version: "20"
|
||||
|
||||
- name: install dependencies
|
||||
run: npm ci
|
||||
@ -35,20 +49,19 @@ jobs:
|
||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm run release ${{ github.event.inputs.type }}
|
||||
run: npm run release ${{ github.event.inputs.versionChange }}
|
||||
|
||||
- name: Get package version
|
||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||
id: package-version
|
||||
run: echo ::set-output name=package_version::v$(jq -r .version package.json)
|
||||
run: echo "package_version=v$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit updated files
|
||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||
uses: EndBug/add-and-commit@v7
|
||||
with:
|
||||
add: '.'
|
||||
message: '[auto] release ${{steps.package-version.outputs.package_version}}'
|
||||
tag: '${{steps.package-version.outputs.package_version}}'
|
||||
add: "."
|
||||
message: "[auto] release ${{env.package_version}}"
|
||||
tag: "${{env.package_version}}"
|
||||
|
||||
- name: Release
|
||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||
@ -56,15 +69,15 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: '${{steps.package-version.outputs.package_version}}'
|
||||
files: '*.zip'
|
||||
tag_name: "${{env.package_version}}"
|
||||
files: "*.zip"
|
||||
|
||||
- name: Upload to chrome store
|
||||
if: ${{ github.event.inputs.onlyUpload != 'firefox' && github.event.inputs.onlyUpload != 'none' }}
|
||||
continue-on-error: true
|
||||
uses: trmcnvn/chrome-addon@v2
|
||||
with:
|
||||
extension: bggfcpfjbdkhfhfmkjpbhnkhnpjjeomc
|
||||
extension: hopghfcljkdgmajlhdfpgpcemcfhbili
|
||||
zip: github-material-icons-chrome-extension.zip
|
||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||
@ -75,7 +88,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
uses: wdzeng/edge-addon@v1
|
||||
with:
|
||||
product-id: d7692295-d84f-4bf5-9447-3cbb6ae29517
|
||||
product-id: fmnacigfpppckhpaafbjdhljbjjclkkj
|
||||
zip-path: github-material-icons-edge-extension.zip
|
||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||
client-secret: ${{ secrets.EDGE_CLIENT_SECRET }}
|
||||
|
54
.github/workflows/update-from-upstream.yml
vendored
54
.github/workflows/update-from-upstream.yml
vendored
@ -1,58 +1,68 @@
|
||||
name: Auto Update when upstream releases
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 3 * * *'
|
||||
- cron: "0 3 * * *"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
get-version:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16.13.0'
|
||||
node-version: "20"
|
||||
|
||||
- name: Fetch release version
|
||||
id: upstream
|
||||
run: |
|
||||
echo ::set-output name=release_tag::$(curl "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/package.json" \
|
||||
| jq -r .version)
|
||||
echo ::set-output name=current_tag::$(<upstream.version)
|
||||
release_tag=$(npm view material-icon-theme version)
|
||||
current_tag=$(npm list material-icon-theme --depth=0 | grep 'material-icon-theme@' | cut -d '@' -f 2)
|
||||
echo "release_tag=$release_tag" >> $GITHUB_ENV
|
||||
echo "current_tag=$current_tag" >> $GITHUB_ENV
|
||||
|
||||
- name: Attempt update
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
if: env.release_tag != env.current_tag
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: npm ci && npm run update
|
||||
|
||||
- name: Get package version
|
||||
id: package-version
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
run: echo ::set-output name=package_version::v$(jq -r .version package.json)
|
||||
if: env.release_tag != env.current_tag
|
||||
run: |
|
||||
echo "package_version=v$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||
|
||||
- name: Commit updated files
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
uses: EndBug/add-and-commit@v7
|
||||
if: env.release_tag != env.current_tag
|
||||
uses: EndBug/add-and-commit@v9
|
||||
with:
|
||||
add: '.'
|
||||
message: '[auto] update to upstream ${{steps.upstream.outputs.release_tag}}'
|
||||
tag: '${{steps.package-version.outputs.package_version}}'
|
||||
add: "."
|
||||
message: "[auto] update to upstream ${{env.release_tag}}"
|
||||
tag: "${{env.package_version}}"
|
||||
|
||||
- name: Release
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
if: env.release_tag != env.current_tag
|
||||
uses: softprops/action-gh-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: '${{steps.package-version.outputs.package_version}}'
|
||||
body: Built with icons from VSCode Material Icon Theme ${{steps.upstream.outputs.release_tag}}
|
||||
files: '*.zip'
|
||||
tag_name: "${{env.package_version}}"
|
||||
body: Built with icons from VSCode Material Icon Theme ${{env.release_tag}}
|
||||
files: "*.zip"
|
||||
|
||||
- name: Upload to chrome store
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
if: env.release_tag != env.current_tag
|
||||
continue-on-error: true
|
||||
uses: trmcnvn/chrome-addon@v2
|
||||
with:
|
||||
@ -63,7 +73,7 @@ jobs:
|
||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||
|
||||
- name: Upload to edge store
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
if: env.release_tag != env.current_tag
|
||||
continue-on-error: true
|
||||
uses: wdzeng/edge-addon@v1
|
||||
with:
|
||||
@ -74,6 +84,6 @@ jobs:
|
||||
access-token-url: ${{ secrets.EDGE_ACCESS_TOKEN_URL }}
|
||||
|
||||
- name: Upload to firefox store
|
||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
||||
if: env.release_tag != env.current_tag
|
||||
continue-on-error: true
|
||||
run: npx web-ext sign -s ./dist/firefox/ --channel=listed --api-key=${{ secrets.FIREFOX_API_JWT_ISSUER }} --api-secret=${{ secrets.FIREFOX_API_JWT_SECRET }}
|
||||
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,11 +1,8 @@
|
||||
dist/
|
||||
temp/
|
||||
.vscode/
|
||||
out/
|
||||
github-material-icons-chrome-extension.zip
|
||||
github-material-icons-firefox-extension.zip
|
||||
github-material-icons-edge-extension.zip
|
||||
src/icon-cache.js
|
||||
src/icon-map.json
|
||||
src/icon-list.json
|
||||
src/language-map.json
|
||||
svg/
|
||||
@ -15,8 +12,6 @@ data/
|
||||
node_modules
|
||||
*.log
|
||||
.idea
|
||||
.cache/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
Thumbs.db
|
||||
|
@ -1,4 +0,0 @@
|
||||
.cache
|
||||
dist
|
||||
src/*.json
|
||||
!src/manifest.json
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"quoteProps": "as-needed",
|
||||
"trailingComma": "es5",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "always",
|
||||
"proseWrap": "preserve",
|
||||
"endOfLine": "lf"
|
||||
}
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["biomejs.biome", "jock.svg", "editorconfig.editorconfig"]
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
<h1 align="center">Material Icons Browser Addon</h1>
|
||||
<h1 align="center">Material Icons for Web</h1>
|
||||
|
||||
<div align="center">
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<a href="https://addons.mozilla.org/de/firefox/addon/material-icons-for-web"><img src="https://github.com/material-extensions/material-icons-browser-addon/raw/main/assets/firefox-addons.png"></a>
|
||||
</p>
|
||||
|
||||
<b>Install directly from the <a href="https://chromewebstore.google.com/detail/material-icons-for-web/hopghfcljkdgmajlhdfpgpcemcfhbili">Chrome Web Store</a> | <a href="https://microsoftedge.microsoft.com/addons/detail/material-icons-for-github/khckkdgomkcjjnpgjmdmbceiddlmiolb">Microsoft Edge Addons Store</a> | <a href="https://addons.mozilla.org/de/firefox/addon/material-icons-for-web">Firefox Addons</a></b></div>
|
||||
<b>Install directly from the <a href="https://chromewebstore.google.com/detail/material-icons-for-web/hopghfcljkdgmajlhdfpgpcemcfhbili">Chrome Web Store</a> | <a href="https://microsoftedge.microsoft.com/addons/detail/fmnacigfpppckhpaafbjdhljbjjclkkj">Microsoft Edge Addons Store</a> | <a href="https://addons.mozilla.org/de/firefox/addon/material-icons-for-web">Firefox Addons</a></b></div>
|
||||
|
||||
---
|
||||
|
||||
@ -56,8 +56,3 @@ Update language-map.json with latest language contributions.
|
||||
npm run build-languages
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
_Special thanks to [@shivapoudel](https://github.com/shivapoudel) for creating and maintaining the Microsoft Edge version of the extension_
|
||||
|
||||
_Original extension developed with [Richard Lam](https://github.com/rlam108)_
|
||||
|
49
biome.jsonc
Normal file
49
biome.jsonc
Normal file
@ -0,0 +1,49 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.2/schema.json",
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80,
|
||||
"attributePosition": "auto"
|
||||
},
|
||||
"organizeImports": { "enabled": true },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": false,
|
||||
"complexity": { "useArrowFunction": "off" },
|
||||
"correctness": { "noUnsafeFinally": "error" },
|
||||
"security": { "noGlobalEval": "error" },
|
||||
"style": {
|
||||
"noVar": "error",
|
||||
"useBlockStatements": "off",
|
||||
"useConst": "error",
|
||||
"useNamingConvention": {
|
||||
"level": "error",
|
||||
"options": { "strictCase": false }
|
||||
}
|
||||
},
|
||||
"suspicious": {
|
||||
"noDoubleEquals": "error",
|
||||
"useNamespaceKeyword": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"jsxQuoteStyle": "single",
|
||||
"quoteProperties": "asNeeded",
|
||||
"trailingCommas": "es5",
|
||||
"semicolons": "always",
|
||||
"arrowParentheses": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"quoteStyle": "single",
|
||||
"attributePosition": "auto"
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
}
|
32510
package-lock.json
generated
32510
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
72
package.json
72
package.json
@ -2,11 +2,10 @@
|
||||
"name": "material-icons-browser-addon",
|
||||
"version": "1.8.15",
|
||||
"description": "Browser Addon that enhances file browsers of version controls with material icons.",
|
||||
"main": "src/main.js",
|
||||
"main": "src/main.ts",
|
||||
"author": {
|
||||
"name": "Philipp Kief",
|
||||
"email": "philipp.kief@gmx.de",
|
||||
"url": "https://pkief.com"
|
||||
"name": "Material Extensions",
|
||||
"url": "https://github.com/material-extensions"
|
||||
},
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/material-extensions/material-icons-browser-addon#readme",
|
||||
@ -17,62 +16,49 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/material-extensions/material-icons-browser-addon/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0",
|
||||
"npm": "^8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"material-icon-theme": "latest",
|
||||
"selector-observer": "2.1.6",
|
||||
"webextension-polyfill": "0.11.0"
|
||||
"webextension-polyfill": "0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/core": "3.5.1",
|
||||
"compare-versions": "3.6.0",
|
||||
"eslint": "8.18.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-prettier": "8.5.0",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"eslint-plugin-jsdoc": "39.3.3",
|
||||
"fs-extra": "10.0.0",
|
||||
"husky": "8.0.1",
|
||||
"json-stable-stringify": "1.0.1",
|
||||
"lint-staged": "13.0.3",
|
||||
"node-fetch": "2.6.7",
|
||||
"@types/fs-extra": "11.0.4",
|
||||
"@types/json-stable-stringify": "1.0.36",
|
||||
"@types/webextension-polyfill": "0.10.7",
|
||||
"esbuild": "0.21.5",
|
||||
"fs-extra": "11.2.0",
|
||||
"husky": "9.0.11",
|
||||
"json-stable-stringify": "1.1.1",
|
||||
"lint-staged": "15.2.7",
|
||||
"npm-run-all": "4.1.5",
|
||||
"parcel-bundler": "1.12.5",
|
||||
"prettier": "2.7.1",
|
||||
"rimraf": "3.0.2",
|
||||
"sharp": "0.28.3",
|
||||
"simple-git": "2.40.0",
|
||||
"svgo": "2.3.1",
|
||||
"web-ext": "7.1.0"
|
||||
"rimraf": "5.0.7",
|
||||
"sharp": "0.33.4",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.5.2",
|
||||
"web-ext": "8.2.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prebuild": "rimraf *.zip ./dist",
|
||||
"build": "run-s build-ext-dependencies build-languages build-src bundle",
|
||||
"build-ext-dependencies": "node ./scripts/build-dependencies.js",
|
||||
"build-languages": "node ./scripts/build-languages.js",
|
||||
"build-src": "node ./scripts/build-src.js",
|
||||
"rebuild-logos": "node ./scripts/build-icons.js",
|
||||
"build": "run-s build-languages build-src bundle",
|
||||
"build-languages": "ts-node ./scripts/build-languages.ts",
|
||||
"build-src": "ts-node ./scripts/build-src.ts",
|
||||
"rebuild-logos": "ts-node ./scripts/build-icons.ts",
|
||||
"bundle": "run-p bundle-edge bundle-chrome bundle-firefox",
|
||||
"bundle-edge": "zip -r -j github-material-icons-edge-extension.zip dist/chrome-edge",
|
||||
"bundle-chrome": "zip -r -j github-material-icons-chrome-extension.zip dist/chrome-edge",
|
||||
"bundle-firefox": "web-ext -s ./dist/firefox/ -n github-material-icons-firefox-extension.zip -a . build --overwrite-dest",
|
||||
"parcel": "parcel build ./src/main.js",
|
||||
"parcel-watch": "parcel watch ./src/main.js",
|
||||
"clean": "rimraf *.zip ./dist ./svg ./.cache ./src/icon-list.json ./src/icon-map.json",
|
||||
"update-manifest-version": "node ./scripts/update-manifest-version.js",
|
||||
"update-upstream-version": "node ./scripts/update-upstream-version",
|
||||
"update-manifest-version": "ts-node ./scripts/update-manifest-version.ts",
|
||||
"update-upstream-version": "ts-node ./scripts/update-upstream-version.ts",
|
||||
"update-package-version": "npm version --no-git-tag-version",
|
||||
"update": "run-s update-upstream-version \"update-package-version patch\" update-manifest-version build",
|
||||
"release": "run-s \"update-package-version {1}\" update-manifest-version build --",
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint --fix .",
|
||||
"format": "prettier --write --ignore-unknown .",
|
||||
"prepare": "husky install"
|
||||
"lint": "npx @biomejs/biome check --write ./src",
|
||||
"format": "npx @biomejs/biome format --write ./src",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": "eslint --fix",
|
||||
"*": "prettier --write --ignore-unknown"
|
||||
"*.ts": "npm run lint",
|
||||
"*": "npm run format"
|
||||
}
|
||||
}
|
||||
|
@ -1,67 +0,0 @@
|
||||
/**
|
||||
* External depedencies
|
||||
*/
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const simpleGit = require('simple-git');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
/**
|
||||
* Internal depedencies
|
||||
*/
|
||||
const srcPath = path.resolve(__dirname, '..', 'src');
|
||||
const vsExtPath = path.resolve(__dirname, '..', 'temp');
|
||||
const destSVGPath = path.resolve(__dirname, '..', 'svg');
|
||||
const commitLockPath = path.resolve(__dirname, '..', 'upstream.commit');
|
||||
|
||||
const vsExtExecOptions = {
|
||||
cwd: vsExtPath,
|
||||
stdio: 'inherit',
|
||||
};
|
||||
const distIconsExecOptions = {
|
||||
cwd: path.resolve(destSVGPath),
|
||||
stdio: 'inherit',
|
||||
};
|
||||
|
||||
async function main() {
|
||||
await fs.remove(vsExtPath);
|
||||
await fs.remove(destSVGPath);
|
||||
await fs.ensureDir(destSVGPath);
|
||||
|
||||
console.log('[1/7] Cloning PKief/vscode-material-icon-theme into temporary cache.');
|
||||
const git = simpleGit();
|
||||
await git.clone(`https://github.com/PKief/vscode-material-icon-theme.git`, vsExtPath, [
|
||||
'--depth',
|
||||
'100', // fetch only last 100 commits. Guesswork, could be too shallow if upstream doesnt release often
|
||||
]);
|
||||
|
||||
const commit = fs.readFileSync(commitLockPath, { encoding: 'utf8' })?.trim();
|
||||
console.log('Checking out to upstream commit:', commit);
|
||||
const upstreamGit = simpleGit(vsExtPath);
|
||||
await upstreamGit.checkout(commit, ['--force']);
|
||||
|
||||
console.log('[2/7] Terminate Git repository in temporary cache.');
|
||||
await fs.remove(path.resolve(vsExtPath, '.git'));
|
||||
|
||||
console.log('[3/7] Install NPM dependencies for VSC extension.');
|
||||
execSync(`npm install --ignore-scripts`, vsExtExecOptions);
|
||||
|
||||
console.log('[4/7] Terminate Git tracking in temporary cache.');
|
||||
await fs.copy(path.resolve(vsExtPath, 'icons'), path.resolve(destSVGPath));
|
||||
|
||||
console.log('[5/7] Optimise extension icons using SVGO.');
|
||||
execSync(`npx svgo -r .`, distIconsExecOptions);
|
||||
|
||||
console.log('[6/7] Run build tasks for VSC extension.');
|
||||
execSync(`npm run build`, vsExtExecOptions);
|
||||
|
||||
console.log('[7/7] Copy file icon configuration to source code directory.');
|
||||
await fs.copy(
|
||||
path.resolve(vsExtPath, 'dist', 'material-icons.json'),
|
||||
path.resolve(srcPath, 'icon-map.json')
|
||||
);
|
||||
|
||||
await fs.remove(vsExtPath);
|
||||
}
|
||||
|
||||
main();
|
@ -1,31 +0,0 @@
|
||||
/**
|
||||
* External depedencies
|
||||
*/
|
||||
const path = require('path');
|
||||
const sharp = require('sharp');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
/**
|
||||
* Internal depedencies
|
||||
*/
|
||||
const svgPath = path.resolve(__dirname, '..', 'src', 'logo.svg');
|
||||
const iconsPath = path.resolve(__dirname, '..', 'src', 'extensionIcons');
|
||||
const targetSizes = [16, 32, 48, 128];
|
||||
|
||||
// Build extension icons.
|
||||
fs.ensureDir(iconsPath).then(generateIcons);
|
||||
|
||||
/**
|
||||
* Generate extension icons.
|
||||
*
|
||||
* @since 1.4.0
|
||||
*/
|
||||
function generateIcons() {
|
||||
targetSizes.forEach((size) => {
|
||||
sharp(svgPath)
|
||||
.png()
|
||||
.resize({ width: size, height: size })
|
||||
.toFile(`${iconsPath}/icon-${size}.png`)
|
||||
.catch(console.error);
|
||||
});
|
||||
}
|
29
scripts/build-icons.ts
Normal file
29
scripts/build-icons.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs-extra';
|
||||
import sharp from 'sharp';
|
||||
|
||||
const svgPath: string = path.resolve(__dirname, '..', 'src', 'logo.svg');
|
||||
const iconsPath: string = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'extensionIcons'
|
||||
);
|
||||
const targetSizes: number[] = [16, 32, 48, 128];
|
||||
|
||||
// Build extension icons.
|
||||
fs.ensureDir(iconsPath).then(generateIcons);
|
||||
|
||||
/**
|
||||
* Generate extension icons.
|
||||
*
|
||||
* @since 1.4.0
|
||||
*/
|
||||
function generateIcons(): void {
|
||||
targetSizes.forEach((size: number) => {
|
||||
sharp(svgPath)
|
||||
.png()
|
||||
.resize({ width: size, height: size })
|
||||
.toFile(`${iconsPath}/icon-${size}.png`);
|
||||
});
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
const path = require('path');
|
||||
const api = require('@octokit/core');
|
||||
const fs = require('fs-extra');
|
||||
const fetch = require('node-fetch');
|
||||
const stringify = require('json-stable-stringify');
|
||||
const iconMap = require('../src/icon-map.json');
|
||||
|
||||
const vsDataPath = path.resolve(__dirname, '..', 'data');
|
||||
const srcPath = path.resolve(__dirname, '..', 'src');
|
||||
|
||||
let index = 0;
|
||||
let total;
|
||||
const items = [];
|
||||
const contributions = [];
|
||||
const languages = [];
|
||||
|
||||
const resultsPerPage = 100; // max 100
|
||||
const octokit = new api.Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
const query = {
|
||||
page: 0,
|
||||
per_page: resultsPerPage,
|
||||
q: 'contributes languages filename:package.json repo:microsoft/vscode',
|
||||
};
|
||||
const GITHUB_RATELIMIT = 6000;
|
||||
|
||||
async function main() {
|
||||
await fs.remove(vsDataPath);
|
||||
await fs.ensureDir(vsDataPath);
|
||||
await fs.remove(path.resolve(srcPath, 'language-map.json'));
|
||||
|
||||
console.log('[1/7] Querying Github API for official VSC language contributions.');
|
||||
queryLanguageContributions();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
async function queryLanguageContributions() {
|
||||
const res = await octokit.request('GET /search/code', query);
|
||||
if (!res.data) throw new Error();
|
||||
query.page = index;
|
||||
index += 1;
|
||||
if (!total) total = res.data.total_count;
|
||||
items.push(...res.data.items);
|
||||
if (resultsPerPage * index >= total) {
|
||||
console.log('[2/7] Fetching Microsoft language contributions from Github.');
|
||||
index = 0;
|
||||
total = items.length;
|
||||
items.forEach(fetchLanguageContribution);
|
||||
} else {
|
||||
setTimeout(queryLanguageContributions, GITHUB_RATELIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLanguageContribution(item) {
|
||||
const rawUrl = item.html_url.replace('/blob/', '/raw/');
|
||||
const resPath = item.path.replace(/[^/]+$/, 'extension.json');
|
||||
const extPath = path.join(vsDataPath, resPath);
|
||||
let extManifest;
|
||||
try {
|
||||
extManifest = await fetch(rawUrl);
|
||||
extManifest = await extManifest.text();
|
||||
} catch (reason) {
|
||||
throw new Error(reason);
|
||||
}
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(extPath));
|
||||
await fs.writeFile(extPath, extManifest, 'utf-8');
|
||||
} catch (reason) {
|
||||
throw new Error(`${reason} (${extPath})`);
|
||||
}
|
||||
items[index] = [extPath, extManifest];
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
console.log('[3/7] Loading VSC language contributions into Node.');
|
||||
index = 0;
|
||||
items.forEach(loadLanguageContribution);
|
||||
}
|
||||
}
|
||||
|
||||
function loadLanguageContribution([extPath, extManifest]) {
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(extManifest.replace(/#\w+_\w+#/g, '0'));
|
||||
} catch (error) {
|
||||
throw new Error(`${error} (${extPath})`);
|
||||
}
|
||||
if (!data.contributes || !data.contributes.languages) {
|
||||
total -= 1;
|
||||
return;
|
||||
}
|
||||
contributions.push(...data.contributes.languages);
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
console.log('[4/7] Processing language contributions for VSC File Icon API compatibility.');
|
||||
index = 0;
|
||||
total = contributions.length;
|
||||
contributions.forEach(processLanguageContribution);
|
||||
}
|
||||
}
|
||||
|
||||
function processLanguageContribution(contribution) {
|
||||
const { id, filenamePatterns } = contribution;
|
||||
let { extensions, filenames } = contribution;
|
||||
extensions = extensions || [];
|
||||
filenames = filenames || [];
|
||||
if (filenamePatterns) {
|
||||
filenamePatterns.forEach((ptn) => {
|
||||
if (/^\*\.[^*/?]+$/.test(ptn)) {
|
||||
extensions.push(ptn.substring(1));
|
||||
}
|
||||
if (/^[^*/?]+$/.test(ptn)) {
|
||||
filenames.push(ptn);
|
||||
}
|
||||
});
|
||||
}
|
||||
extensions = extensions
|
||||
.map((ext) => (ext.charAt(0) === '.' ? ext.substring(1) : ext))
|
||||
.filter((ext) => !/\*|\/|\?/.test(ext));
|
||||
filenames = filenames.filter((name) => !/\*|\/|\?/.test(name));
|
||||
if (!filenames.length && !extensions.length) {
|
||||
total -= 1;
|
||||
return;
|
||||
}
|
||||
const language = languages.find((lang) => lang.id === id);
|
||||
if (language) {
|
||||
language.filenames.push(...filenames);
|
||||
language.extensions.push(...extensions);
|
||||
} else {
|
||||
languages.push({ id, extensions, filenames });
|
||||
}
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
console.log('[5/7] Mapping language contributions into file icon configuration.');
|
||||
index = 0;
|
||||
total = languages.length;
|
||||
languages.forEach(mapLanguageContribution);
|
||||
}
|
||||
}
|
||||
|
||||
const languageMap = {};
|
||||
languageMap.fileExtensions = {};
|
||||
languageMap.fileNames = {};
|
||||
|
||||
function mapLanguageContribution(lang) {
|
||||
const langIcon = iconMap.languageIds[lang.id];
|
||||
lang.extensions.forEach((ext) => {
|
||||
const iconName = iconMap.fileExtensions[ext] || langIcon;
|
||||
if (!iconMap.fileExtensions[ext] && iconName && iconMap.iconDefinitions[iconName]) {
|
||||
languageMap.fileExtensions[ext] = iconName;
|
||||
}
|
||||
});
|
||||
lang.filenames.forEach((name) => {
|
||||
const iconName = iconMap.fileNames[name] || langIcon;
|
||||
if (
|
||||
!iconMap.fileNames[name] &&
|
||||
!(name.startsWith('.') && iconMap.fileExtensions[name.substring(1)]) &&
|
||||
iconName &&
|
||||
iconMap.iconDefinitions[iconName]
|
||||
) {
|
||||
languageMap.fileNames[name] = iconName;
|
||||
}
|
||||
});
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
index = 0;
|
||||
generateLanguageMap();
|
||||
}
|
||||
}
|
||||
|
||||
async function generateLanguageMap() {
|
||||
console.log('[6/7] Writing language contribution map to icon configuration file.');
|
||||
fs.writeFileSync(
|
||||
path.resolve(srcPath, 'language-map.json'),
|
||||
stringify(languageMap, { space: ' ' })
|
||||
);
|
||||
console.log('[7/7] Deleting language contribution cache.');
|
||||
await fs.remove(vsDataPath);
|
||||
}
|
231
scripts/build-languages.ts
Normal file
231
scripts/build-languages.ts
Normal file
@ -0,0 +1,231 @@
|
||||
import * as path from 'path';
|
||||
import { Octokit } from '@octokit/core';
|
||||
import * as fs from 'fs-extra';
|
||||
import stringify from 'json-stable-stringify';
|
||||
import iconMap from 'material-icon-theme/dist/material-icons.json';
|
||||
|
||||
const iconMapTyped: {
|
||||
languageIds: { [key: string]: string };
|
||||
fileExtensions: { [key: string]: string };
|
||||
fileNames: { [key: string]: string };
|
||||
iconDefinitions: { [key: string]: any };
|
||||
} = iconMap;
|
||||
|
||||
interface LanguageContribution {
|
||||
id: string;
|
||||
extensions?: string[];
|
||||
filenames?: string[];
|
||||
filenamePatterns?: string[];
|
||||
}
|
||||
|
||||
interface Language {
|
||||
id: string;
|
||||
extensions: string[];
|
||||
filenames: string[];
|
||||
}
|
||||
|
||||
const vsDataPath: string = path.resolve(__dirname, '..', 'data');
|
||||
const srcPath: string = path.resolve(__dirname, '..', 'src');
|
||||
|
||||
let index: number = 0;
|
||||
let total: number;
|
||||
const items: Array<[string, string]> = [];
|
||||
const contributions: LanguageContribution[] = [];
|
||||
const languages: Language[] = [];
|
||||
|
||||
const resultsPerPage: number = 100; // max 100
|
||||
const octokit: Octokit = new Octokit({
|
||||
auth: process.env.GITHUB_TOKEN,
|
||||
});
|
||||
// biome-ignore lint/style/useNamingConvention: per_page is a valid name
|
||||
const query: { page: number; per_page: number; q: string } = {
|
||||
page: 0,
|
||||
// biome-ignore lint/style/useNamingConvention: per_page is a valid name
|
||||
per_page: resultsPerPage,
|
||||
q: 'contributes languages filename:package.json repo:microsoft/vscode',
|
||||
};
|
||||
const GITHUB_RATELIMIT: number = 6000;
|
||||
|
||||
async function main(): Promise<void> {
|
||||
await fs.remove(vsDataPath);
|
||||
await fs.ensureDir(vsDataPath);
|
||||
await fs.remove(path.resolve(srcPath, 'language-map.json'));
|
||||
|
||||
console.log(
|
||||
'[1/7] Querying Github API for official VSC language contributions.'
|
||||
);
|
||||
queryLanguageContributions();
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
async function queryLanguageContributions(): Promise<void> {
|
||||
const res = await octokit.request('GET /search/code', query);
|
||||
if (!res.data) throw new Error();
|
||||
query.page = index;
|
||||
index += 1;
|
||||
if (!total) total = res.data.total_count;
|
||||
items.push(
|
||||
...res.data.items.map(
|
||||
(item) => [item.html_url, item.path] as [string, string]
|
||||
)
|
||||
);
|
||||
if (resultsPerPage * index >= total) {
|
||||
console.log('[2/7] Fetching Microsoft language contributions from Github.');
|
||||
index = 0;
|
||||
total = items.length;
|
||||
items.forEach(([htmlUrl, path]) =>
|
||||
fetchLanguageContribution(htmlUrl, path)
|
||||
);
|
||||
} else {
|
||||
setTimeout(queryLanguageContributions, GITHUB_RATELIMIT);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchLanguageContribution(
|
||||
htmlUrl: string,
|
||||
itemPath: string
|
||||
): Promise<void> {
|
||||
const rawUrl: string = htmlUrl.replace('/blob/', '/raw/');
|
||||
const resPath: string = itemPath.replace(/[^/]+$/, 'extension.json');
|
||||
const extPath: string = path.join(vsDataPath, resPath);
|
||||
let extManifest: string;
|
||||
try {
|
||||
const response = await fetch(rawUrl, {});
|
||||
extManifest = await response.text();
|
||||
} catch (reason) {
|
||||
throw new Error(`${reason}`);
|
||||
}
|
||||
try {
|
||||
await fs.ensureDir(path.dirname(extPath));
|
||||
await fs.writeFile(extPath, extManifest, 'utf-8');
|
||||
} catch (reason) {
|
||||
throw new Error(`${reason} (${extPath})`);
|
||||
}
|
||||
items[index] = [extPath, extManifest];
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
console.log('[3/7] Loading VSC language contributions into Node.');
|
||||
index = 0;
|
||||
items.forEach(([extPath, extManifest]) =>
|
||||
loadLanguageContribution(extPath, extManifest)
|
||||
);
|
||||
|
||||
console.log(
|
||||
'[4/7] Processing language contributions for VSC File Icon API compatibility.'
|
||||
);
|
||||
index = 0;
|
||||
total = contributions.length;
|
||||
contributions.forEach(processLanguageContribution);
|
||||
}
|
||||
}
|
||||
|
||||
function loadLanguageContribution(extPath: string, extManifest: string): void {
|
||||
let data: any;
|
||||
try {
|
||||
data = JSON.parse(extManifest.replace(/#\w+_\w+#/g, '0'));
|
||||
} catch (error) {
|
||||
throw new Error(`${error} (${extPath})`);
|
||||
}
|
||||
if (!data.contributes?.languages) {
|
||||
return;
|
||||
}
|
||||
contributions.push(...data.contributes.languages);
|
||||
}
|
||||
|
||||
function processLanguageContribution(contribution: LanguageContribution): void {
|
||||
const { id, filenamePatterns } = contribution;
|
||||
let { extensions, filenames } = contribution;
|
||||
extensions = extensions || [];
|
||||
filenames = filenames || [];
|
||||
if (filenamePatterns) {
|
||||
filenamePatterns.forEach((ptn) => {
|
||||
if (/^\*\.[^*/?]+$/.test(ptn)) {
|
||||
extensions?.push(ptn.substring(1));
|
||||
}
|
||||
if (/^[^*/?]+$/.test(ptn)) {
|
||||
filenames?.push(ptn);
|
||||
}
|
||||
});
|
||||
}
|
||||
extensions = extensions
|
||||
.map((ext) => (ext.charAt(0) === '.' ? ext.substring(1) : ext))
|
||||
.filter((ext) => !/\*|\/|\?/.test(ext));
|
||||
filenames = filenames.filter((name) => !/\*|\/|\?/.test(name));
|
||||
if (!filenames.length && !extensions.length) {
|
||||
total -= 1;
|
||||
return;
|
||||
}
|
||||
const language: Language | undefined = languages.find(
|
||||
(lang) => lang.id === id
|
||||
);
|
||||
if (language) {
|
||||
language.filenames.push(...filenames);
|
||||
language.extensions.push(...extensions);
|
||||
} else {
|
||||
languages.push({ id, extensions, filenames });
|
||||
}
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
console.log(
|
||||
'[5/7] Mapping language contributions into file icon configuration.'
|
||||
);
|
||||
index = 0;
|
||||
total = languages.length;
|
||||
languages.forEach(mapLanguageContribution);
|
||||
}
|
||||
}
|
||||
|
||||
const languageMap: {
|
||||
fileExtensions: { [key: string]: string };
|
||||
fileNames: { [key: string]: string };
|
||||
} = {
|
||||
fileExtensions: {},
|
||||
fileNames: {},
|
||||
};
|
||||
|
||||
function mapLanguageContribution(lang: Language): void {
|
||||
// Assuming iconMap is defined elsewhere in the code or imported
|
||||
const langIcon: string | undefined = iconMapTyped.languageIds[lang.id];
|
||||
lang.extensions.forEach((ext) => {
|
||||
const iconName: string | undefined =
|
||||
iconMapTyped.fileExtensions[ext] || langIcon;
|
||||
if (
|
||||
!iconMapTyped.fileExtensions[ext] &&
|
||||
iconName &&
|
||||
iconMapTyped.iconDefinitions[iconName]
|
||||
) {
|
||||
languageMap.fileExtensions[ext] = iconName;
|
||||
}
|
||||
});
|
||||
lang.filenames.forEach((name) => {
|
||||
const iconName: string | undefined =
|
||||
iconMapTyped.fileNames[name] || langIcon;
|
||||
if (
|
||||
!iconMapTyped.fileNames[name] &&
|
||||
!(
|
||||
name.startsWith('.') && iconMapTyped.fileExtensions[name.substring(1)]
|
||||
) &&
|
||||
iconName &&
|
||||
iconMapTyped.iconDefinitions[iconName]
|
||||
) {
|
||||
languageMap.fileNames[name] = iconName;
|
||||
}
|
||||
});
|
||||
index += 1;
|
||||
if (index === total) {
|
||||
generateLanguageMap();
|
||||
}
|
||||
}
|
||||
|
||||
async function generateLanguageMap(): Promise<void> {
|
||||
console.log(
|
||||
'[6/7] Writing language contribution map to icon configuration file.'
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.resolve(srcPath, 'language-map.json'),
|
||||
stringify(languageMap, { space: ' ' })
|
||||
);
|
||||
console.log('[7/7] Deleting language contribution cache.');
|
||||
await fs.remove(vsDataPath);
|
||||
}
|
@ -1,98 +0,0 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
const Parcel = require('parcel-bundler');
|
||||
|
||||
const destSVGPath = path.resolve(__dirname, '..', 'svg');
|
||||
const distBasePath = path.resolve(__dirname, '..', 'dist');
|
||||
const srcPath = path.resolve(__dirname, '..', 'src');
|
||||
|
||||
/** Create icons cache. */
|
||||
async function consolidateSVGFiles() {
|
||||
console.log('[1/2] Generate icon cache for extension.');
|
||||
await fs
|
||||
.copy(path.resolve(srcPath, 'custom'), destSVGPath)
|
||||
.then(() => fs.readdir(destSVGPath))
|
||||
.then((files) => Object.fromEntries(files.map((filename) => [filename, filename])))
|
||||
.then((iconsDict) => fs.writeJSON(path.resolve(srcPath, 'icon-list.json'), iconsDict));
|
||||
}
|
||||
|
||||
function bundleJS(outDir, entryFile) {
|
||||
const parcelOptions = {
|
||||
watch: false,
|
||||
minify: true,
|
||||
sourceMaps: false,
|
||||
outDir,
|
||||
};
|
||||
const bundler = new Parcel(entryFile, parcelOptions);
|
||||
return bundler.bundle();
|
||||
}
|
||||
|
||||
function src(distPath) {
|
||||
console.log('[2/2] Bundle extension manifest, images and main script.');
|
||||
|
||||
const copyIcons = fs.copy(destSVGPath, distPath);
|
||||
|
||||
const bundleMainScript = () => bundleJS(distPath, path.resolve(srcPath, 'main.js'));
|
||||
const bundlePopupScript = () =>
|
||||
bundleJS(distPath, path.resolve(srcPath, 'ui', 'popup', 'settings-popup.js'));
|
||||
const bundleOptionsScript = () =>
|
||||
bundleJS(distPath, path.resolve(srcPath, 'ui', 'options', 'options.js'));
|
||||
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) =>
|
||||
fs.copy(path.resolve(srcPath, 'ui', 'popup', file), path.resolve(distPath, file))
|
||||
)
|
||||
);
|
||||
|
||||
const copyOptions = Promise.all(
|
||||
['options.html', 'options.css'].map((file) =>
|
||||
fs.copy(path.resolve(srcPath, 'ui', 'options', file), path.resolve(distPath, file))
|
||||
)
|
||||
);
|
||||
|
||||
const copyStyles = fs.copy(
|
||||
path.resolve(srcPath, 'injected-styles.css'),
|
||||
path.resolve(distPath, 'injected-styles.css')
|
||||
);
|
||||
|
||||
const copyExtensionLogos = fs.copy(path.resolve(srcPath, 'extensionIcons'), distPath);
|
||||
|
||||
return Promise.all([
|
||||
copyExtensionLogos,
|
||||
copyOptions,
|
||||
copyPopup,
|
||||
copyStyles,
|
||||
copyIcons,
|
||||
bundleAll,
|
||||
]);
|
||||
}
|
||||
|
||||
function buildManifest(distPath, manifestName) {
|
||||
return Promise.all([
|
||||
fs.readJson(path.resolve(srcPath, 'manifests', 'base.json')),
|
||||
fs.readJson(path.resolve(srcPath, 'manifests', manifestName)),
|
||||
])
|
||||
.then(([base, custom]) => ({ ...base, ...custom }))
|
||||
.then((manifest) =>
|
||||
fs.writeJson(path.resolve(distPath, 'manifest.json'), manifest, { spaces: 2 })
|
||||
);
|
||||
}
|
||||
|
||||
function buildDist(name, manifestName) {
|
||||
const distPath = path.resolve(distBasePath, name);
|
||||
|
||||
return fs
|
||||
.ensureDir(distPath)
|
||||
.then(consolidateSVGFiles)
|
||||
.then(() => src(distPath))
|
||||
.then(() => buildManifest(distPath, manifestName))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
buildDist('firefox', 'firefox.json').then(() => buildDist('chrome-edge', 'chrome-edge.json'));
|
135
scripts/build-src.ts
Normal file
135
scripts/build-src.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import * as path from 'path';
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as fs from 'fs-extra';
|
||||
|
||||
const destSVGPath: string = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'material-icon-theme',
|
||||
'icons'
|
||||
);
|
||||
const distBasePath: string = path.resolve(__dirname, '..', 'dist');
|
||||
const srcPath: string = path.resolve(__dirname, '..', 'src');
|
||||
|
||||
/** Create icons cache. */
|
||||
async function consolidateSVGFiles(): Promise<void> {
|
||||
console.log('[1/2] Generate icon cache for extension.');
|
||||
await fs
|
||||
.copy(path.resolve(srcPath, 'custom'), destSVGPath)
|
||||
.then(() => fs.readdir(destSVGPath))
|
||||
.then((files) =>
|
||||
Object.fromEntries(files.map((filename) => [filename, filename]))
|
||||
)
|
||||
.then((iconsDict) =>
|
||||
fs.writeJSON(path.resolve(srcPath, 'icon-list.json'), iconsDict)
|
||||
);
|
||||
}
|
||||
|
||||
function bundleJS(
|
||||
outDir: string,
|
||||
entryFile: string
|
||||
): Promise<esbuild.BuildResult> {
|
||||
const buildOptions: esbuild.BuildOptions = {
|
||||
entryPoints: [entryFile],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: false,
|
||||
outdir: outDir,
|
||||
};
|
||||
return esbuild.build(buildOptions);
|
||||
}
|
||||
|
||||
function src(
|
||||
distPath: string
|
||||
): Promise<(void | esbuild.BuildResult | void[])[]> {
|
||||
console.log('[2/2] Bundle extension manifest, images and main script.');
|
||||
|
||||
const copyIcons: Promise<void> = fs.copy(destSVGPath, distPath);
|
||||
|
||||
const bundleMainScript = (): Promise<esbuild.BuildResult> =>
|
||||
bundleJS(distPath, path.resolve(srcPath, 'main.ts'));
|
||||
const bundlePopupScript = (): Promise<esbuild.BuildResult> =>
|
||||
bundleJS(
|
||||
distPath,
|
||||
path.resolve(srcPath, 'ui', 'popup', 'settings-popup.ts')
|
||||
);
|
||||
const bundleOptionsScript = (): Promise<esbuild.BuildResult> =>
|
||||
bundleJS(distPath, path.resolve(srcPath, 'ui', 'options', 'options.ts'));
|
||||
const bundleBackgroundScript = (): Promise<esbuild.BuildResult> =>
|
||||
bundleJS(distPath, path.resolve(srcPath, 'background', 'background.ts'));
|
||||
|
||||
const bundleAll: Promise<esbuild.BuildResult> = bundleMainScript()
|
||||
.then(bundlePopupScript)
|
||||
.then(bundleOptionsScript)
|
||||
.then(bundleBackgroundScript);
|
||||
|
||||
const copyPopup: Promise<void[]> = Promise.all(
|
||||
[
|
||||
'settings-popup.html',
|
||||
'settings-popup.css',
|
||||
'settings-popup.github-logo.svg',
|
||||
].map((file) =>
|
||||
fs.copy(
|
||||
path.resolve(srcPath, 'ui', 'popup', file),
|
||||
path.resolve(distPath, file)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const copyOptions: Promise<void[]> = Promise.all(
|
||||
['options.html', 'options.css'].map((file) =>
|
||||
fs.copy(
|
||||
path.resolve(srcPath, 'ui', 'options', file),
|
||||
path.resolve(distPath, file)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const copyStyles: Promise<void> = fs.copy(
|
||||
path.resolve(srcPath, 'injected-styles.css'),
|
||||
path.resolve(distPath, 'injected-styles.css')
|
||||
);
|
||||
|
||||
const copyExtensionLogos: Promise<void> = fs.copy(
|
||||
path.resolve(srcPath, 'extensionIcons'),
|
||||
distPath
|
||||
);
|
||||
|
||||
return Promise.all([
|
||||
copyExtensionLogos,
|
||||
copyOptions,
|
||||
copyPopup,
|
||||
copyStyles,
|
||||
copyIcons,
|
||||
bundleAll,
|
||||
]);
|
||||
}
|
||||
|
||||
function buildManifest(distPath: string, manifestName: string): Promise<void> {
|
||||
return Promise.all([
|
||||
fs.readJson(path.resolve(srcPath, 'manifests', 'base.json')),
|
||||
fs.readJson(path.resolve(srcPath, 'manifests', manifestName)),
|
||||
])
|
||||
.then(([base, custom]) => ({ ...base, ...custom }))
|
||||
.then((manifest) =>
|
||||
fs.writeJson(path.resolve(distPath, 'manifest.json'), manifest, {
|
||||
spaces: 2,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function buildDist(name: string, manifestName: string): Promise<void> {
|
||||
const distPath: string = path.resolve(distBasePath, name);
|
||||
|
||||
return fs
|
||||
.ensureDir(distPath)
|
||||
.then(consolidateSVGFiles)
|
||||
.then(() => src(distPath))
|
||||
.then(() => buildManifest(distPath, manifestName))
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
buildDist('firefox', 'firefox.json').then(() =>
|
||||
buildDist('chrome-edge', 'chrome-edge.json')
|
||||
);
|
@ -1,20 +0,0 @@
|
||||
/**
|
||||
* Copies version from package.json into src/manifest.json
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
const package = require(path.resolve(__dirname, '..', 'package.json'));
|
||||
|
||||
const manifestPath = path.resolve(__dirname, '..', 'src', 'manifests', 'base.json');
|
||||
const manifest = require(manifestPath);
|
||||
|
||||
const updatedManifest = { ...manifest, version: package.version };
|
||||
const updatedManifestStr = `${JSON.stringify(updatedManifest, null, 2)}\n`;
|
||||
|
||||
fs.writeFile(manifestPath, updatedManifestStr)
|
||||
.then(() => {
|
||||
console.log(`Updated manifest.json version to ${package.version}`);
|
||||
})
|
||||
.catch(console.error);
|
34
scripts/update-manifest-version.ts
Normal file
34
scripts/update-manifest-version.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
const packageJsonPath: string = path.resolve(__dirname, '..', 'package.json');
|
||||
const manifestPath: string = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'src',
|
||||
'manifests',
|
||||
'base.json'
|
||||
);
|
||||
|
||||
const updateManifestVersion = async (): Promise<void> => {
|
||||
const packageJsonData: string = await fs.readFile(packageJsonPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const packageJson = JSON.parse(packageJsonData);
|
||||
|
||||
const manifestData: string = await fs.readFile(manifestPath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const manifest = JSON.parse(manifestData);
|
||||
|
||||
const updatedManifest = {
|
||||
...manifest,
|
||||
version: packageJson.version,
|
||||
};
|
||||
const updatedManifestStr: string = `${JSON.stringify(updatedManifest, null, 2)}\n`;
|
||||
|
||||
await fs.writeFile(manifestPath, updatedManifestStr);
|
||||
console.log(`Updated manifest.json version to ${packageJson.version}`);
|
||||
};
|
||||
|
||||
updateManifestVersion().catch(console.error);
|
@ -1,68 +0,0 @@
|
||||
const fetch = require('node-fetch');
|
||||
const path = require('path');
|
||||
const api = require('@octokit/core');
|
||||
const compareVersions = require('compare-versions');
|
||||
const fs = require('fs/promises');
|
||||
|
||||
const upstreamVersionFilePath = path.resolve(__dirname, '..', 'upstream.version');
|
||||
const upstreamCommitFilePath = path.resolve(__dirname, '..', 'upstream.commit');
|
||||
|
||||
/**
|
||||
* Gets latest VSCode Extension release version by parsing it's most recent 100 commit msgs
|
||||
*
|
||||
* returns version string or undefined
|
||||
*
|
||||
* @returns {Promise<string>} The current version of the upstream repository.
|
||||
*/
|
||||
const getUpstreamVersion = () =>
|
||||
fetch('https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/package.json')
|
||||
.then((res) => res.json())
|
||||
.then((package) => package.version);
|
||||
|
||||
const octokit = new api.Octokit();
|
||||
const getUpstreamCommit = () =>
|
||||
octokit
|
||||
.request('GET /repos/PKief/vscode-material-icon-theme/commits', { per_page: 1 })
|
||||
.then((res) => res.data?.[0].sha);
|
||||
|
||||
const getLastUpstreamVersion = () =>
|
||||
fs.readFile(upstreamVersionFilePath, { encoding: 'utf8' }).then((data) => data.trim());
|
||||
|
||||
const updateReadmeBadge = async (version) => {
|
||||
const readmeFilePath = path.resolve(__dirname, '..', 'README.md');
|
||||
|
||||
const readme = await fs.readFile(readmeFilePath, { encoding: 'utf8' });
|
||||
|
||||
const versionRgx = /(badge\/[\w_]+-v)\d+\.\d+\.\d+/;
|
||||
const replacement = `$1${version}`;
|
||||
|
||||
const updatedReadme = readme.replace(versionRgx, replacement);
|
||||
|
||||
return fs.writeFile(readmeFilePath, updatedReadme);
|
||||
};
|
||||
|
||||
const run = async () => {
|
||||
const latestVersion = await getUpstreamVersion();
|
||||
const lastSeenVersion = await getLastUpstreamVersion();
|
||||
const latestUpstreamCommit = await getUpstreamCommit();
|
||||
|
||||
console.log(`Latest upstream version: ${latestVersion}`);
|
||||
console.log(`Current repository version: ${lastSeenVersion}`);
|
||||
|
||||
if (!latestVersion || compareVersions.compare(lastSeenVersion, latestVersion, '>=')) {
|
||||
// exit script with an error. Simplifies chaining of shell commands only in case updates are found
|
||||
console.log('No update necessary.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Updating upstream version in "/upstream.version"');
|
||||
await fs.writeFile(upstreamVersionFilePath, latestVersion);
|
||||
|
||||
console.log('Updating upstream commit sha in "/upstream.commit"');
|
||||
await fs.writeFile(upstreamCommitFilePath, latestUpstreamCommit);
|
||||
|
||||
console.log('Updating upstream version badge in README');
|
||||
await updateReadmeBadge(latestVersion);
|
||||
};
|
||||
|
||||
run();
|
50
scripts/update-upstream-version.ts
Normal file
50
scripts/update-upstream-version.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
/**
|
||||
* Gets latest VSCode Extension release version by parsing its most recent 100 commit msgs
|
||||
*
|
||||
* @returns {Promise<string>} The current version of the upstream repository.
|
||||
*/
|
||||
const getUpstreamVersion = async (): Promise<string> => {
|
||||
const packagePath: string = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'node_modules',
|
||||
'material-icon-theme',
|
||||
'package.json'
|
||||
);
|
||||
const packageData: string = await fs.readFile(packagePath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const packageJson = JSON.parse(packageData);
|
||||
return packageJson.version;
|
||||
};
|
||||
|
||||
/**
|
||||
* Updates the version badge in the README file.
|
||||
*
|
||||
* @param {string} version - The new version to update the badge to.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const updateReadmeBadge = async (version: string): Promise<void> => {
|
||||
const readmeFilePath: string = path.resolve(__dirname, '..', 'README.md');
|
||||
const readme: string = await fs.readFile(readmeFilePath, {
|
||||
encoding: 'utf8',
|
||||
});
|
||||
const versionRgx: RegExp = /(badge\/[\w_]+-v)\d+\.\d+\.\d+/;
|
||||
const replacement: string = `$1${version}`;
|
||||
const updatedReadme: string = readme.replace(versionRgx, replacement);
|
||||
|
||||
await fs.writeFile(readmeFilePath, updatedReadme);
|
||||
};
|
||||
|
||||
/**
|
||||
* Main function to run the update process.
|
||||
*/
|
||||
const run = async (): Promise<void> => {
|
||||
const latestVersion: string = await getUpstreamVersion();
|
||||
await updateReadmeBadge(latestVersion);
|
||||
};
|
||||
|
||||
run().catch(console.error);
|
@ -1,13 +1,21 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
|
||||
Browser.runtime.onMessage.addListener((message) => {
|
||||
type Message = {
|
||||
event: string;
|
||||
data: {
|
||||
host: string;
|
||||
tabId: number;
|
||||
};
|
||||
};
|
||||
|
||||
Browser.runtime.onMessage.addListener((message: Message) => {
|
||||
if (message.event === 'request-access') {
|
||||
const perm = {
|
||||
const perm: Browser.Permissions.Permissions = {
|
||||
permissions: ['activeTab'],
|
||||
origins: [`*://${message.data.host}/*`],
|
||||
};
|
||||
|
||||
Browser.permissions.request(perm).then((granted) => {
|
||||
Browser.permissions.request(perm).then((granted: boolean) => {
|
||||
if (!granted) {
|
||||
return;
|
||||
}
|
@ -1 +0,0 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13 9h5.5L13 3.5V9M6 2h8l6 6v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4c0-1.11.89-2 2-2m5 2H6v16h12v-9h-7V4z" fill="#42a5f5"/></svg>
|
Before Width: | Height: | Size: 193 B |
@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" fill="#42a5f5" opacity=".745"/><path d="M16.972 10.757v2.641h-6.561v5.281h6.561v2.641l6.562-5.281-6.562-5.282z" opacity=".81" fill="#c5e5fd"/></svg>
|
||||
<svg version="1.1" viewBox="0 0 32 32" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><path d="M13.84376,7.53645l-1.28749-1.0729A2,2,0,0,0,11.27591,6H4A2,2,0,0,0,2,8V24a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V10a2,2,0,0,0-2-2H15.12412A2,2,0,0,1,13.84376,7.53645Z" fill="#90a4ae"/><g transform="translate(3.233,3.34)" fill="#eceff1"><path d="m20.767 9.66v4h-8v6h8v4l8-7z" fill="#eceff1" /></g></svg>
|
||||
|
Before Width: | Height: | Size: 321 B After Width: | Height: | Size: 399 B |
@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 4H4c-1.11 0-2 .89-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z" fill="#42a5f5"/></svg>
|
Before Width: | Height: | Size: 195 B |
@ -1,10 +0,0 @@
|
||||
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 });
|
||||
});
|
19
src/lib/custom-providers.ts
Normal file
19
src/lib/custom-providers.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
import { Provider } from '../models';
|
||||
|
||||
export const getCustomProviders = (): Promise<
|
||||
Record<string, (() => Provider) | string>
|
||||
> =>
|
||||
Browser.storage.sync
|
||||
.get('customProviders')
|
||||
.then((data) => data.customProviders || {});
|
||||
|
||||
export const addCustomProvider = (
|
||||
name: string,
|
||||
handler: (() => Provider) | string
|
||||
) =>
|
||||
getCustomProviders().then((customProviders) => {
|
||||
customProviders[name] = handler;
|
||||
|
||||
return Browser.storage.sync.set({ customProviders });
|
||||
});
|
@ -1,6 +1,8 @@
|
||||
import { getConfig, onConfigChange } from './userConfig';
|
||||
import { getConfig, onConfigChange } from './user-config';
|
||||
|
||||
const setSizeAttribute = (iconSize) =>
|
||||
export type IconSize = 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
const setSizeAttribute = (iconSize: IconSize) =>
|
||||
document.body.setAttribute(`data-material-icons-extension-size`, iconSize);
|
||||
|
||||
export const initIconSizes = () => {
|
@ -1,238 +0,0 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
import iconsList from '../icon-list.json';
|
||||
import iconMap from '../icon-map.json';
|
||||
import languageMap from '../language-map.json';
|
||||
|
||||
/**
|
||||
* A helper function to check if an object has a key value, without including prooerties from the prototype chain.
|
||||
*
|
||||
* @see https://eslint.org/docs/latest/rules/no-prototype-builtins
|
||||
* @param {object} obj An object to check for a key.
|
||||
* @param {string} key A string represeing a key to find in the object.
|
||||
* @returns {boolean} Whether the object contains the key or not.
|
||||
*/
|
||||
function objectHas(obj, key) {
|
||||
return Object.prototype.hasOwnProperty.call(obj, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace file/folder icons.
|
||||
*
|
||||
* @param {HTMLElement} itemRow Item Row.
|
||||
* @param {object} provider Git Provider specs.
|
||||
* @param {string | null} iconPack active icon pack. Selectable by user
|
||||
* @returns {void}
|
||||
*/
|
||||
export function replaceIconInRow(itemRow, provider, iconPack) {
|
||||
// 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.
|
||||
|
||||
// SVG to be replaced.
|
||||
const iconEl = itemRow.querySelector(provider.selectors.icon);
|
||||
if (iconEl?.getAttribute('data-material-icons-extension')) return;
|
||||
if (iconEl) replaceIcon(iconEl, fileName, itemRow, provider, iconPack);
|
||||
}
|
||||
|
||||
function replaceIcon(iconEl, fileName, itemRow, provider, iconPack) {
|
||||
// Get Directory or Submodule type.
|
||||
const isDir = provider.getIsDirectory({ row: itemRow, icon: iconEl });
|
||||
const isSubmodule = provider.getIsSubmodule({ row: itemRow, icon: iconEl });
|
||||
const isSymlink = provider.getIsSymlink({ row: itemRow, icon: iconEl });
|
||||
const lowerFileName = fileName.toLowerCase();
|
||||
|
||||
// Get file extensions.
|
||||
const fileExtensions = [];
|
||||
// Avoid doing an explosive combination of extensions for very long filenames
|
||||
// (most file systems do not allow files > 255 length) with lots of `.` characters
|
||||
// https://github.com/microsoft/vscode/issues/116199
|
||||
if (fileName.length <= 255) {
|
||||
for (let i = 0; i < fileName.length; i += 1) {
|
||||
if (fileName[i] === '.') fileExtensions.push(fileName.slice(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Get icon name.
|
||||
let iconName = lookForMatch(
|
||||
fileName,
|
||||
lowerFileName,
|
||||
fileExtensions,
|
||||
isDir,
|
||||
isSubmodule,
|
||||
isSymlink
|
||||
); // returns icon name if found or undefined.
|
||||
|
||||
const isLightTheme = provider.getIsLightTheme();
|
||||
if (isLightTheme) {
|
||||
iconName = lookForLightMatch(iconName, fileName, fileExtensions, isDir); // returns icon name if found for light mode or undefined.
|
||||
}
|
||||
|
||||
replaceElementWithIcon(iconEl, iconName, fileName, iconPack, provider);
|
||||
}
|
||||
|
||||
export function replaceElementWithIcon(iconEl, iconName, fileName, iconPack, provider) {
|
||||
// Get folder icon from active icon pack.
|
||||
const svgFileName = lookForIconPackMatch(iconPack, fileName.toLowerCase()) ?? iconName;
|
||||
|
||||
if (!svgFileName) return;
|
||||
|
||||
const newSVG = document.createElement('img');
|
||||
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 = Browser.runtime.getURL(`${svgFileName}.svg`);
|
||||
|
||||
provider.replaceIcon(iconEl, 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[]} fileExtensions File extensions.
|
||||
* @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, fileExtensions, isDir, isSubmodule, isSymlink) {
|
||||
if (isSubmodule) return 'folder-git';
|
||||
if (isSymlink) return 'folder-symlink';
|
||||
|
||||
// If it's a file.
|
||||
if (!isDir) {
|
||||
// First look in fileNames
|
||||
if (objectHas(iconMap.fileNames, fileName)) return iconMap.fileNames[fileName];
|
||||
|
||||
// Then check all lowercase
|
||||
if (objectHas(iconMap.fileNames, lowerFileName)) return iconMap.fileNames[lowerFileName];
|
||||
|
||||
// Look for extension in fileExtensions and languageIds.
|
||||
for (const ext of fileExtensions) {
|
||||
if (objectHas(iconMap.fileExtensions, ext)) return iconMap.fileExtensions[ext];
|
||||
if (objectHas(iconMap.languageIds, ext)) return iconMap.languageIds[ext];
|
||||
}
|
||||
|
||||
// Look for filename and extension in VSCode language map.
|
||||
if (objectHas(languageMap.fileNames, fileName)) return languageMap.fileNames[fileName];
|
||||
if (objectHas(languageMap.fileNames, lowerFileName))
|
||||
return languageMap.fileNames[lowerFileName];
|
||||
for (const ext of fileExtensions) {
|
||||
if (objectHas(languageMap.fileExtensions, ext)) return languageMap.fileExtensions[ext];
|
||||
}
|
||||
|
||||
// Fallback into default file if no matches.
|
||||
return 'file';
|
||||
}
|
||||
|
||||
// Otherwise, it's a folder.
|
||||
// First look in folderNames.
|
||||
if (objectHas(iconMap.folderNames, fileName)) return iconMap.folderNames[fileName];
|
||||
|
||||
// Then check all lowercase.
|
||||
if (objectHas(iconMap.folderNames, lowerFileName)) return iconMap.folderNames[lowerFileName];
|
||||
|
||||
// Fallback into default folder if no matches.
|
||||
return 'folder';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup for matched light file/folder icon name.
|
||||
*
|
||||
* @since 1.4.0
|
||||
* @param {string} iconName Icon name.
|
||||
* @param {string} fileName File name.
|
||||
* @param {string[]} fileExtensions File extension.
|
||||
* @param {boolean} isDir Check if directory or file type.
|
||||
* @returns {string} The matched icon name.
|
||||
*/
|
||||
function lookForLightMatch(iconName, fileName, fileExtensions, 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.
|
||||
for (const ext of fileExtensions) {
|
||||
if (iconMap.light.fileExtensions[ext] && !isDir) return iconMap.light.fileExtensions[ext];
|
||||
}
|
||||
|
||||
return iconName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup for matched icon from active icon pack.
|
||||
*
|
||||
* @since 1.4.0
|
||||
* @param {string | null} iconPack active icon pack. Selectable by user
|
||||
* @param {string} lowerFileName Lowercase file name.
|
||||
* @returns {string | null} The matched icon name.
|
||||
*/
|
||||
function lookForIconPackMatch(iconPack, lowerFileName) {
|
||||
if (!iconPack) return null;
|
||||
switch (iconPack) {
|
||||
case 'angular':
|
||||
if (objectHas(iconsList, `folder-angular-${lowerFileName}.svg`))
|
||||
return `folder-angular-${lowerFileName}`;
|
||||
break;
|
||||
case 'angular_ngrx':
|
||||
if (objectHas(iconsList, `folder-ngrx-${lowerFileName}.svg`))
|
||||
return `folder-ngrx-${lowerFileName}`;
|
||||
if (objectHas(iconsList, `folder-angular-${lowerFileName}.svg`))
|
||||
return `folder-angular-${lowerFileName}`;
|
||||
break;
|
||||
case 'react':
|
||||
case 'react_redux':
|
||||
if (objectHas(iconsList, `folder-react-${lowerFileName}.svg`)) {
|
||||
return `folder-react-${lowerFileName}`;
|
||||
}
|
||||
if (objectHas(iconsList, `folder-redux-${lowerFileName}.svg`)) {
|
||||
return `folder-redux-${lowerFileName}`;
|
||||
}
|
||||
break;
|
||||
case 'vue':
|
||||
case 'vue_vuex':
|
||||
if (objectHas(iconsList, `folder-vuex-${lowerFileName}.svg`)) {
|
||||
return `folder-vuex-${lowerFileName}`;
|
||||
}
|
||||
if (objectHas(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;
|
||||
}
|
208
src/lib/replace-icon.ts
Normal file
208
src/lib/replace-icon.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import iconMap from 'material-icon-theme/dist/material-icons.json';
|
||||
import Browser from 'webextension-polyfill';
|
||||
import iconsList from '../icon-list.json';
|
||||
import languageMap from '../language-map.json';
|
||||
import { Provider } from '../models';
|
||||
|
||||
const iconMapTyped = iconMap as {
|
||||
fileNames: Record<string, string>;
|
||||
folderNames: Record<string, string>;
|
||||
fileExtensions: Record<string, string>;
|
||||
languageIds: Record<string, string>;
|
||||
light: {
|
||||
fileNames: Record<string, string>;
|
||||
folderNames: Record<string, string>;
|
||||
fileExtensions: Record<string, string>;
|
||||
};
|
||||
};
|
||||
const iconsListTyped = iconsList as Record<string, string>;
|
||||
const languageMapTyped = languageMap as {
|
||||
fileExtensions: Record<string, string>;
|
||||
fileNames: Record<string, string>;
|
||||
};
|
||||
|
||||
export function replaceIconInRow(
|
||||
itemRow: HTMLElement,
|
||||
provider: Provider,
|
||||
iconPack: string | null
|
||||
): void {
|
||||
const fileName = itemRow
|
||||
.querySelector(provider.selectors.filename)
|
||||
?.textContent?.split('/')[0]
|
||||
.trim();
|
||||
if (!fileName) return;
|
||||
|
||||
const iconEl = itemRow.querySelector(
|
||||
provider.selectors.icon
|
||||
) as HTMLElement | null;
|
||||
if (iconEl?.getAttribute('data-material-icons-extension')) return;
|
||||
if (iconEl) replaceIcon(iconEl, fileName, itemRow, provider, iconPack);
|
||||
}
|
||||
|
||||
function replaceIcon(
|
||||
iconEl: HTMLElement,
|
||||
fileName: string,
|
||||
itemRow: HTMLElement,
|
||||
provider: Provider,
|
||||
iconPack: string | null
|
||||
): void {
|
||||
const isDir = provider.getIsDirectory({ row: itemRow, icon: iconEl });
|
||||
const isSubmodule = provider.getIsSubmodule({ row: itemRow, icon: iconEl });
|
||||
const isSymlink = provider.getIsSymlink({ row: itemRow, icon: iconEl });
|
||||
const lowerFileName = fileName.toLowerCase();
|
||||
|
||||
const fileExtensions: string[] = [];
|
||||
if (fileName.length <= 255) {
|
||||
for (let i = 0; i < fileName.length; i += 1) {
|
||||
if (fileName[i] === '.') fileExtensions.push(fileName.slice(i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
let iconName = lookForMatch(
|
||||
fileName,
|
||||
lowerFileName,
|
||||
fileExtensions,
|
||||
isDir,
|
||||
isSubmodule,
|
||||
isSymlink
|
||||
);
|
||||
|
||||
const isLightTheme = provider.getIsLightTheme();
|
||||
if (isLightTheme) {
|
||||
iconName = lookForLightMatch(iconName, fileName, fileExtensions, isDir);
|
||||
}
|
||||
|
||||
replaceElementWithIcon(iconEl, iconName, fileName, iconPack, provider);
|
||||
}
|
||||
|
||||
export function replaceElementWithIcon(
|
||||
iconEl: HTMLElement,
|
||||
iconName: string | undefined,
|
||||
fileName: string,
|
||||
iconPack: string | null,
|
||||
provider: Provider
|
||||
): void {
|
||||
const svgFileName =
|
||||
lookForIconPackMatch(iconPack, fileName.toLowerCase()) ?? iconName;
|
||||
|
||||
if (!svgFileName) return;
|
||||
|
||||
const newSVG = document.createElement('img');
|
||||
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 = Browser.runtime.getURL(`${svgFileName}.svg`);
|
||||
|
||||
provider.replaceIcon(iconEl, newSVG);
|
||||
}
|
||||
|
||||
function lookForMatch(
|
||||
fileName: string,
|
||||
lowerFileName: string,
|
||||
fileExtensions: string[],
|
||||
isDir: boolean,
|
||||
isSubmodule: boolean,
|
||||
isSymlink: boolean
|
||||
): string | undefined {
|
||||
if (isSubmodule) return 'folder-git';
|
||||
if (isSymlink) return 'folder-symlink';
|
||||
|
||||
if (!isDir) {
|
||||
if (iconMapTyped.fileNames[fileName])
|
||||
return iconMapTyped.fileNames[fileName];
|
||||
if (iconMapTyped.fileNames[lowerFileName])
|
||||
return iconMapTyped.fileNames[lowerFileName];
|
||||
|
||||
for (const ext of fileExtensions) {
|
||||
if (iconMapTyped.fileExtensions[ext])
|
||||
return iconMapTyped.fileExtensions[ext];
|
||||
if (iconMapTyped.languageIds[ext]) return iconMapTyped.languageIds[ext];
|
||||
}
|
||||
|
||||
if (languageMapTyped.fileNames[fileName])
|
||||
return languageMapTyped.fileNames[fileName];
|
||||
if (languageMapTyped.fileNames[lowerFileName])
|
||||
return languageMapTyped.fileNames[lowerFileName];
|
||||
for (const ext of fileExtensions) {
|
||||
if (languageMapTyped.fileExtensions[ext])
|
||||
return languageMapTyped.fileExtensions[ext];
|
||||
}
|
||||
|
||||
return 'file';
|
||||
}
|
||||
|
||||
if (iconMapTyped.folderNames[fileName])
|
||||
return iconMapTyped.folderNames[fileName];
|
||||
if (iconMapTyped.folderNames[lowerFileName])
|
||||
return iconMapTyped.folderNames[lowerFileName];
|
||||
|
||||
return 'folder';
|
||||
}
|
||||
|
||||
function lookForLightMatch(
|
||||
iconName: string | undefined,
|
||||
fileName: string,
|
||||
fileExtensions: string[],
|
||||
isDir: boolean
|
||||
): string | undefined {
|
||||
if (iconMapTyped.light.fileNames[fileName] && !isDir)
|
||||
return iconMapTyped.light.fileNames[fileName];
|
||||
if (iconMapTyped.light.folderNames[fileName] && isDir)
|
||||
return iconMapTyped.light.folderNames[fileName];
|
||||
|
||||
for (const ext of fileExtensions) {
|
||||
if (iconMapTyped.light.fileExtensions[ext] && !isDir)
|
||||
return iconMapTyped.light.fileExtensions[ext];
|
||||
}
|
||||
|
||||
return iconName;
|
||||
}
|
||||
|
||||
function lookForIconPackMatch(
|
||||
iconPack: string | null,
|
||||
lowerFileName: string
|
||||
): string | null {
|
||||
if (!iconPack) return null;
|
||||
switch (iconPack) {
|
||||
case 'angular':
|
||||
if (iconsListTyped[`folder-angular-${lowerFileName}.svg`])
|
||||
return `folder-angular-${lowerFileName}`;
|
||||
break;
|
||||
case 'angular_ngrx':
|
||||
if (iconsListTyped[`folder-ngrx-${lowerFileName}.svg`])
|
||||
return `folder-ngrx-${lowerFileName}`;
|
||||
if (iconsListTyped[`folder-angular-${lowerFileName}.svg`])
|
||||
return `folder-angular-${lowerFileName}`;
|
||||
break;
|
||||
case 'react':
|
||||
case 'react_redux':
|
||||
if (iconsListTyped[`folder-react-${lowerFileName}.svg`])
|
||||
return `folder-react-${lowerFileName}`;
|
||||
if (iconsListTyped[`folder-redux-${lowerFileName}.svg`])
|
||||
return `folder-redux-${lowerFileName}`;
|
||||
break;
|
||||
case 'vue':
|
||||
case 'vue_vuex':
|
||||
if (iconsListTyped[`folder-vuex-${lowerFileName}.svg`])
|
||||
return `folder-vuex-${lowerFileName}`;
|
||||
if (iconsListTyped[`folder-vue-${lowerFileName}.svg`])
|
||||
return `folder-vue-${lowerFileName}`;
|
||||
if (lowerFileName === 'nuxt') return `folder-nuxt`;
|
||||
break;
|
||||
case 'nest':
|
||||
if (/\.controller\.(t|j)s$/.test(lowerFileName)) return `nest-controller`;
|
||||
if (/\.middleware\.(t|j)s$/.test(lowerFileName)) return 'nest-middleware';
|
||||
if (/\.module\.(t|j)s$/.test(lowerFileName)) return 'nest-module';
|
||||
if (/\.service\.(t|j)s$/.test(lowerFileName)) return 'nest-service';
|
||||
if (/\.decorator\.(t|j)s$/.test(lowerFileName)) return 'nest-decorator';
|
||||
if (/\.pipe\.(t|j)s$/.test(lowerFileName)) return 'nest-pipe';
|
||||
if (/\.filter\.(t|j)s$/.test(lowerFileName)) return 'nest-filter';
|
||||
if (/\.gateway\.(t|j)s$/.test(lowerFileName)) return 'nest-gateway';
|
||||
if (/\.guard\.(t|j)s$/.test(lowerFileName)) return 'nest-guard';
|
||||
if (/\.resolver\.(t|j)s$/.test(lowerFileName)) return 'nest-resolver';
|
||||
return null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { observe } from 'selector-observer';
|
||||
|
||||
import { replaceIconInRow, replaceElementWithIcon } 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, iconPack) => {
|
||||
observe(gitProvider.selectors.row, {
|
||||
add(row) {
|
||||
const callback = () => replaceIconInRow(row, gitProvider, iconPack);
|
||||
rushFirst(90, callback);
|
||||
gitProvider.onAdd(row, callback);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const replaceAllIcons = (provider, iconPack) =>
|
||||
document.querySelectorAll('img[data-material-icons-extension-iconname]').forEach((iconEl) => {
|
||||
const iconName = iconEl.getAttribute('data-material-icons-extension-iconname');
|
||||
const fileName = iconEl.getAttribute('data-material-icons-extension-filename');
|
||||
if (iconName) replaceElementWithIcon(iconEl, iconName, fileName, iconPack, provider);
|
||||
});
|
52
src/lib/replace-icons.ts
Normal file
52
src/lib/replace-icons.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { observe } from 'selector-observer';
|
||||
import { Provider } from '../models';
|
||||
import { replaceElementWithIcon, replaceIconInRow } from './replace-icon';
|
||||
|
||||
let executions = 0;
|
||||
let timerID: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const rushFirst = (rushBatch: number, callback: () => void): void => {
|
||||
if (executions <= rushBatch) {
|
||||
callback();
|
||||
setTimeout(callback, 20);
|
||||
executions += 1;
|
||||
} else {
|
||||
setTimeout(callback, 0);
|
||||
if (timerID !== null) {
|
||||
clearTimeout(timerID);
|
||||
}
|
||||
timerID = setTimeout(() => {
|
||||
executions = 0;
|
||||
}, 1000);
|
||||
}
|
||||
};
|
||||
|
||||
export const observePage = (gitProvider: Provider, iconPack: string): void => {
|
||||
observe(gitProvider.selectors.row, {
|
||||
add(row) {
|
||||
const callback = () =>
|
||||
replaceIconInRow(row as HTMLElement, gitProvider, iconPack);
|
||||
rushFirst(90, callback);
|
||||
gitProvider.onAdd(row as HTMLElement, callback);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const replaceAllIcons = (provider: Provider, iconPack: string) =>
|
||||
document
|
||||
.querySelectorAll('img[data-material-icons-extension-iconname]')
|
||||
.forEach((iconEl) => {
|
||||
const iconName = iconEl.getAttribute(
|
||||
'data-material-icons-extension-iconname'
|
||||
);
|
||||
const fileName =
|
||||
iconEl.getAttribute('data-material-icons-extension-filename') ?? '';
|
||||
if (iconName)
|
||||
replaceElementWithIcon(
|
||||
iconEl as HTMLElement,
|
||||
iconName,
|
||||
fileName,
|
||||
iconPack,
|
||||
provider
|
||||
);
|
||||
});
|
57
src/lib/user-config.ts
Normal file
57
src/lib/user-config.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
|
||||
export type UserConfig = {
|
||||
iconPack: string;
|
||||
iconSize: string;
|
||||
extEnabled: boolean;
|
||||
};
|
||||
|
||||
const hardDefaults: UserConfig = {
|
||||
iconPack: 'react',
|
||||
iconSize: 'md',
|
||||
extEnabled: true,
|
||||
};
|
||||
|
||||
export const getConfig = (
|
||||
configName: keyof UserConfig,
|
||||
domain = window.location.hostname,
|
||||
useDefault = true
|
||||
) =>
|
||||
Browser.storage.sync
|
||||
.get({
|
||||
// get custom domain config (if not getting default).
|
||||
[`${domain !== 'default' ? domain : 'SKIP'}:${configName}`]: null,
|
||||
// also get user default as fallback
|
||||
[`default:${configName}`]: hardDefaults[configName],
|
||||
})
|
||||
.then(
|
||||
({
|
||||
[`${domain}:${configName}`]: value,
|
||||
[`default:${configName}`]: fallback,
|
||||
}) => value ?? (useDefault ? fallback : null)
|
||||
);
|
||||
|
||||
export const setConfig = (
|
||||
configName: keyof UserConfig,
|
||||
value: unknown,
|
||||
domain = window.location.hostname
|
||||
) =>
|
||||
Browser.storage.sync.set({
|
||||
[`${domain}:${configName}`]: value,
|
||||
});
|
||||
|
||||
export const clearConfig = (
|
||||
configName: keyof UserConfig,
|
||||
domain = window.location.hostname
|
||||
) => Browser.storage.sync.remove(`${domain}:${configName}`);
|
||||
|
||||
export const onConfigChange = (
|
||||
configName: keyof UserConfig,
|
||||
handler: Function,
|
||||
domain = window.location.hostname
|
||||
) =>
|
||||
Browser.storage.onChanged.addListener(
|
||||
(changes) =>
|
||||
changes[`${domain}:${configName}`]?.newValue !== undefined &&
|
||||
handler(changes[`${domain}:${configName}`]?.newValue)
|
||||
);
|
@ -1,35 +0,0 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
|
||||
const hardDefaults = {
|
||||
iconPack: 'react',
|
||||
iconSize: 'md',
|
||||
extEnabled: true,
|
||||
};
|
||||
|
||||
export const getConfig = (config, domain = window.location.hostname, useDefault = true) =>
|
||||
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 }) =>
|
||||
value ?? (useDefault ? fallback : null)
|
||||
);
|
||||
|
||||
export const setConfig = (config, value, domain = window.location.hostname) =>
|
||||
Browser.storage.sync.set({
|
||||
[`${domain}:${config}`]: value,
|
||||
});
|
||||
|
||||
export const clearConfig = (config, domain = window.location.hostname) =>
|
||||
Browser.storage.sync.remove(`${domain}:${config}`);
|
||||
|
||||
export const onConfigChange = (config, handler, domain = window.location.hostname) =>
|
||||
Browser.storage.onChanged.addListener(
|
||||
(changes) =>
|
||||
changes[`${domain}:${config}`]?.newValue !== undefined &&
|
||||
handler(changes[`${domain}:${config}`]?.newValue)
|
||||
);
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
||||
/**
|
||||
* A helper function to check if an object has a key value, without including prooerties from the prototype chain.
|
||||
*/
|
||||
export function objectHas(obj: object, key: string) {
|
||||
return obj.hasOwnProperty(key);
|
||||
}
|
49
src/main.js
49
src/main.js
@ -1,49 +0,0 @@
|
||||
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';
|
||||
|
||||
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();
|
79
src/main.ts
Normal file
79
src/main.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
import { initIconSizes } from './lib/icon-sizes';
|
||||
import { observePage, replaceAllIcons } from './lib/replace-icons';
|
||||
import { getConfig, onConfigChange } from './lib/user-config';
|
||||
import { Provider } from './models';
|
||||
import { getGitProvider } from './providers';
|
||||
|
||||
interface Possibilities {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
const init = (): void => {
|
||||
initIconSizes();
|
||||
|
||||
const { href } = window.location;
|
||||
|
||||
getGitProvider(href).then((Provider: Provider | null) => {
|
||||
Promise.all([
|
||||
getConfig('iconPack'),
|
||||
getConfig('extEnabled'),
|
||||
getConfig('extEnabled', 'default'),
|
||||
]).then(
|
||||
([iconPack, extEnabled, globalExtEnabled]: [
|
||||
string,
|
||||
boolean,
|
||||
boolean,
|
||||
]) => {
|
||||
if (!globalExtEnabled || !extEnabled || !Provider) return;
|
||||
observePage(Provider, iconPack);
|
||||
onConfigChange('iconPack', (newIconPack: string) =>
|
||||
replaceAllIcons(Provider, newIconPack)
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
type Handlers = {
|
||||
init: () => void;
|
||||
guessProvider: (possibilities: Possibilities) => string | null;
|
||||
};
|
||||
|
||||
const handlers: Handlers = {
|
||||
init,
|
||||
guessProvider: (possibilities: Possibilities): string | null => {
|
||||
for (const [name, selector] of Object.entries(possibilities)) {
|
||||
if (document.querySelector(selector)) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
Browser.runtime.onMessage.addListener(
|
||||
(
|
||||
message: { cmd: keyof Handlers; args?: any[] },
|
||||
_: Browser.Runtime.MessageSender,
|
||||
sendResponse: (response?: any) => void
|
||||
) => {
|
||||
if (!handlers[message.cmd]) {
|
||||
return sendResponse(null);
|
||||
}
|
||||
|
||||
if (message.cmd === 'init') {
|
||||
handlers.init();
|
||||
return sendResponse(null);
|
||||
}
|
||||
|
||||
if (message.cmd === 'guessProvider') {
|
||||
const result = handlers[message.cmd](
|
||||
(message.args || []) as unknown as Possibilities
|
||||
);
|
||||
return sendResponse(result);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
init();
|
1
src/models/index.ts
Normal file
1
src/models/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './provider';
|
18
src/models/provider.ts
Normal file
18
src/models/provider.ts
Normal file
@ -0,0 +1,18 @@
|
||||
export interface Provider {
|
||||
name: string;
|
||||
domains: { host: string; test: RegExp }[];
|
||||
selectors: {
|
||||
filename: string;
|
||||
icon: string;
|
||||
row: string;
|
||||
detect: string | null;
|
||||
};
|
||||
canSelfHost: boolean;
|
||||
isCustom: boolean;
|
||||
onAdd: (row: HTMLElement, callback: () => void) => void;
|
||||
getIsDirectory: (params: { row: HTMLElement; icon: HTMLElement }) => boolean;
|
||||
getIsSubmodule: (params: { row: HTMLElement; icon: HTMLElement }) => boolean;
|
||||
getIsSymlink: (params: { row: HTMLElement; icon: HTMLElement }) => boolean;
|
||||
getIsLightTheme: () => boolean;
|
||||
replaceIcon: (oldIcon: HTMLElement, newIcon: HTMLElement) => void;
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
import { Provider } from '../models';
|
||||
|
||||
/** The name of the class used to hide the pseudo element `:before` on Azure */
|
||||
const HIDE_PSEUDO_CLASS = 'material-icons-exension-hide-pseudo';
|
||||
|
||||
export default function azure() {
|
||||
export default function azure(): Provider {
|
||||
return {
|
||||
name: 'azure',
|
||||
domains: [
|
||||
@ -25,11 +27,13 @@ export default function azure() {
|
||||
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
|
||||
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'),
|
||||
getIsSymlink: ({ icon }) =>
|
||||
icon.classList.contains('ms-Icon--PageArrowRight'),
|
||||
replaceIcon: (svgEl, newSVG) => {
|
||||
newSVG.style.display = 'inline-flex';
|
||||
newSVG.style.height = '1rem';
|
||||
@ -41,7 +45,7 @@ export default function azure() {
|
||||
|
||||
// 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()) {
|
||||
if (svgEl.hasChildNodes() && svgEl.firstChild !== null) {
|
||||
svgEl.replaceChild(newSVG, svgEl.firstChild);
|
||||
} else {
|
||||
svgEl.appendChild(newSVG);
|
||||
@ -51,11 +55,13 @@ export default function azure() {
|
||||
// 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) => {
|
||||
const mutationCallback = (mutationsList: MutationRecord[]) => {
|
||||
// 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')
|
||||
Array.from(mutation.addedNodes).some(
|
||||
(node) => node.nodeName === 'IMG'
|
||||
)
|
||||
);
|
||||
|
||||
// If the mutation was not caused by the extension, run the icon replacement
|
||||
@ -66,7 +72,11 @@ export default function azure() {
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(mutationCallback);
|
||||
observer.observe(row, { attributes: true, childList: true, subtree: true });
|
||||
observer.observe(row, {
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
export default function bitbucket() {
|
||||
import { Provider } from '../models';
|
||||
|
||||
export default function bitbucket(): Provider {
|
||||
return {
|
||||
name: 'bitbucket',
|
||||
domains: [
|
||||
@ -18,8 +20,12 @@ export default function bitbucket() {
|
||||
canSelfHost: true,
|
||||
isCustom: false,
|
||||
getIsLightTheme: () => true, // No dark mode available for bitbucket currently
|
||||
getIsDirectory: ({ icon }) => icon.parentNode?.getAttribute('aria-label') === 'Directory,',
|
||||
getIsSubmodule: ({ icon }) => icon.parentNode?.getAttribute('aria-label') === 'Submodule,',
|
||||
getIsDirectory: ({ icon }) =>
|
||||
(icon.parentNode as HTMLElement)?.getAttribute('aria-label') ===
|
||||
'Directory,',
|
||||
getIsSubmodule: ({ icon }) =>
|
||||
(icon.parentNode as HTMLElement)?.getAttribute('aria-label') ===
|
||||
'Submodule,',
|
||||
getIsSymlink: () => false, // There appears to be no way to determine this for bitbucket
|
||||
replaceIcon: (svgEl, newSVG) => {
|
||||
newSVG.style.overflow = 'hidden';
|
||||
@ -34,10 +40,10 @@ export default function bitbucket() {
|
||||
(attr) =>
|
||||
attr !== 'src' &&
|
||||
!/^data-material-icons-extension/.test(attr) &&
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||
);
|
||||
|
||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
||||
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||
},
|
||||
onAdd: () => {},
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
export default function gitea() {
|
||||
import { Provider } from '../models';
|
||||
|
||||
export default function gitea(): Provider {
|
||||
return {
|
||||
name: 'gitea',
|
||||
domains: [
|
||||
@ -17,9 +19,12 @@ export default function gitea() {
|
||||
canSelfHost: true,
|
||||
isCustom: false,
|
||||
getIsLightTheme: () => false,
|
||||
getIsDirectory: ({ icon }) => icon.classList.contains('octicon-file-directory-fill'),
|
||||
getIsSubmodule: ({ icon }) => icon.classList.contains('octicon-file-submodule'),
|
||||
getIsSymlink: ({ icon }) => icon.classList.contains('octicon-file-symlink-file'),
|
||||
getIsDirectory: ({ icon }) =>
|
||||
icon.classList.contains('octicon-file-directory-fill'),
|
||||
getIsSubmodule: ({ icon }) =>
|
||||
icon.classList.contains('octicon-file-submodule'),
|
||||
getIsSymlink: ({ icon }) =>
|
||||
icon.classList.contains('octicon-file-symlink-file'),
|
||||
replaceIcon: (svgEl, newSVG) => {
|
||||
svgEl
|
||||
.getAttributeNames()
|
||||
@ -27,10 +32,10 @@ export default function gitea() {
|
||||
(attr) =>
|
||||
attr !== 'src' &&
|
||||
!/^data-material-icons-extension/.test(attr) &&
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||
);
|
||||
|
||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
||||
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||
},
|
||||
onAdd: () => {},
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
export default function gitee() {
|
||||
import { Provider } from '../models';
|
||||
|
||||
export default function gitee(): Provider {
|
||||
return {
|
||||
name: 'gitee',
|
||||
domains: [
|
||||
@ -11,7 +13,8 @@ export default function gitee() {
|
||||
// File list row, README header, file view header
|
||||
row: '#git-project-content .tree-content .row.tree-item, .file_title, .blob-description',
|
||||
// File name table cell, Submodule name table cell, file view header
|
||||
filename: '.tree-list-item > a, .tree-item-submodule-name a, span.file_name',
|
||||
filename:
|
||||
'.tree-list-item > a, .tree-item-submodule-name a, span.file_name',
|
||||
// The iconfont icon not including the delete button icon in the file view header
|
||||
icon: 'i.iconfont:not(.icon-delete)',
|
||||
// Element by which to detect if the tested domain is gitee.
|
||||
@ -30,13 +33,13 @@ export default function gitee() {
|
||||
(attr) =>
|
||||
attr !== 'src' &&
|
||||
!/^data-material-icons-extension/.test(attr) &&
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||
);
|
||||
|
||||
newSVG.style.height = '28px';
|
||||
newSVG.style.width = '18px';
|
||||
|
||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
||||
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||
},
|
||||
onAdd: () => {},
|
||||
};
|
@ -1,4 +1,6 @@
|
||||
export default function github() {
|
||||
import { Provider } from '../models';
|
||||
|
||||
export default function github(): Provider {
|
||||
return {
|
||||
name: 'github',
|
||||
domains: [
|
||||
@ -29,14 +31,17 @@ export default function github() {
|
||||
canSelfHost: true,
|
||||
isCustom: false,
|
||||
getIsLightTheme: () =>
|
||||
document.querySelector('html').getAttribute('data-color-mode') === 'light',
|
||||
document.querySelector('html')?.getAttribute('data-color-mode') ===
|
||||
'light',
|
||||
getIsDirectory: ({ icon }) =>
|
||||
icon.getAttribute('aria-label') === 'Directory' ||
|
||||
icon.classList.contains('octicon-file-directory-fill') ||
|
||||
icon.classList.contains('octicon-file-directory-open-fill') ||
|
||||
icon.classList.contains('icon-directory'),
|
||||
getIsSubmodule: ({ icon }) => icon.getAttribute('aria-label') === 'Submodule',
|
||||
getIsSymlink: ({ icon }) => icon.getAttribute('aria-label') === 'Symlink Directory',
|
||||
getIsSubmodule: ({ icon }) =>
|
||||
icon.getAttribute('aria-label') === 'Submodule',
|
||||
getIsSymlink: ({ icon }) =>
|
||||
icon.getAttribute('aria-label') === 'Symlink Directory',
|
||||
replaceIcon: (svgEl, newSVG) => {
|
||||
svgEl
|
||||
.getAttributeNames()
|
||||
@ -44,7 +49,7 @@ export default function github() {
|
||||
(attr) =>
|
||||
attr !== 'src' &&
|
||||
!/^data-material-icons-extension/.test(attr) &&
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||
);
|
||||
|
||||
const prevEl = svgEl.previousElementSibling;
|
@ -1,4 +1,6 @@
|
||||
export default function gitlab() {
|
||||
import { Provider } from '../models';
|
||||
|
||||
export default function gitlab(): Provider {
|
||||
return {
|
||||
name: 'gitlab',
|
||||
domains: [
|
||||
@ -20,11 +22,14 @@ export default function gitlab() {
|
||||
},
|
||||
canSelfHost: true,
|
||||
isCustom: false,
|
||||
getIsLightTheme: () => !document.querySelector('body').classList.contains('gl-dark'),
|
||||
getIsDirectory: ({ icon }) => icon.getAttribute('data-testid') === 'folder-icon',
|
||||
getIsLightTheme: () =>
|
||||
!document.querySelector('body')?.classList.contains('gl-dark'),
|
||||
getIsDirectory: ({ icon }) =>
|
||||
icon.getAttribute('data-testid') === 'folder-icon',
|
||||
getIsSubmodule: ({ row }) =>
|
||||
row.querySelector('a')?.classList.contains('is-submodule') || false,
|
||||
getIsSymlink: ({ icon }) => icon.getAttribute('data-testid') === 'symlink-icon',
|
||||
getIsSymlink: ({ icon }) =>
|
||||
icon.getAttribute('data-testid') === 'symlink-icon',
|
||||
replaceIcon: (svgEl, newSVG) => {
|
||||
svgEl
|
||||
.getAttributeNames()
|
||||
@ -32,13 +37,13 @@ export default function gitlab() {
|
||||
(attr) =>
|
||||
attr !== 'src' &&
|
||||
!/^data-material-icons-extension/.test(attr) &&
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||
);
|
||||
|
||||
newSVG.style.height = '16px';
|
||||
newSVG.style.width = '16px';
|
||||
|
||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
||||
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||
},
|
||||
onAdd: () => {},
|
||||
};
|
@ -1,13 +1,14 @@
|
||||
import github from './github';
|
||||
import bitbucket from './bitbucket';
|
||||
import azure from './azure';
|
||||
import gitea from './gitea';
|
||||
import gitlab from './gitlab';
|
||||
import gitee from './gitee';
|
||||
import sourceforge from './sourceforge';
|
||||
import { getCustomProviders } from '../lib/custom-providers';
|
||||
import { Provider } from '../models';
|
||||
import azure from './azure';
|
||||
import bitbucket from './bitbucket';
|
||||
import gitea from './gitea';
|
||||
import gitee from './gitee';
|
||||
import github from './github';
|
||||
import gitlab from './gitlab';
|
||||
import sourceforge from './sourceforge';
|
||||
|
||||
export const providers = {
|
||||
export const providers: Record<string, () => Provider> = {
|
||||
azure,
|
||||
bitbucket,
|
||||
gitea,
|
||||
@ -17,7 +18,7 @@ export const providers = {
|
||||
sourceforge,
|
||||
};
|
||||
|
||||
export const providerConfig = {};
|
||||
export const providerConfig: Record<string, Provider> = {};
|
||||
|
||||
for (const provider of Object.values(providers)) {
|
||||
const cfg = provider();
|
||||
@ -25,17 +26,17 @@ for (const provider of Object.values(providers)) {
|
||||
providerConfig[cfg.name] = cfg;
|
||||
}
|
||||
|
||||
function regExpEscape(str) {
|
||||
return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, '\\$&');
|
||||
function regExpEscape(value: string) {
|
||||
return value.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add custom git provider
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {string|CallableFunction} handler
|
||||
*/
|
||||
export const addGitProvider = (name, handler) => {
|
||||
export const addGitProvider = (
|
||||
name: string,
|
||||
handler: (() => Provider) | string
|
||||
) => {
|
||||
handler = typeof handler === 'string' ? providers[handler] : handler;
|
||||
|
||||
const provider = handler();
|
||||
@ -64,12 +65,8 @@ export const getGitProviders = () =>
|
||||
|
||||
/**
|
||||
* Get all selectors and functions specific to the Git provider
|
||||
*
|
||||
* @param {string} href Url of current tab
|
||||
* @param domain
|
||||
* @returns {object} All of the values needed for the provider
|
||||
*/
|
||||
export const getGitProvider = (domain) => {
|
||||
export const getGitProvider = (domain: string) => {
|
||||
if (!domain.startsWith('http')) {
|
||||
domain = new URL(`http://${domain}`).host;
|
||||
} else {
|
@ -1,4 +1,6 @@
|
||||
export default function sourceforge() {
|
||||
import { Provider } from '../models';
|
||||
|
||||
export default function sourceforge(): Provider {
|
||||
return {
|
||||
name: 'sourceforge',
|
||||
domains: [
|
||||
@ -42,11 +44,15 @@ export default function sourceforge() {
|
||||
newSVG.style.height = '14px';
|
||||
newSVG.style.width = '14px';
|
||||
|
||||
iconOrAnchor.parentNode.replaceChild(newSVG, iconOrAnchor);
|
||||
iconOrAnchor.parentNode?.replaceChild(newSVG, iconOrAnchor);
|
||||
}
|
||||
// For the files list, use the anchor element instead of the icon because in some cases there is no icon
|
||||
else {
|
||||
if (iconOrAnchor.querySelector('img[data-material-icons-extension="icon"]')) {
|
||||
if (
|
||||
iconOrAnchor.querySelector(
|
||||
'img[data-material-icons-extension="icon"]'
|
||||
)
|
||||
) {
|
||||
// only replace/prepend the icon once
|
||||
return;
|
||||
}
|
||||
@ -57,7 +63,7 @@ export default function sourceforge() {
|
||||
const svgEl = iconOrAnchor.querySelector('svg');
|
||||
|
||||
if (svgEl) {
|
||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
||||
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||
} else {
|
||||
iconOrAnchor.prepend(newSVG);
|
||||
}
|
@ -48,7 +48,7 @@
|
||||
</div>
|
||||
|
||||
<div id="footer" class="centered">
|
||||
<a href="https://github.com/Claudiohbsantos/github-material-icons-extension" target="_blank"
|
||||
<a href="https://github.com/material-extensions/material-icons-browser-addon" target="_blank"
|
||||
><img src="settings-popup.github-logo.svg"
|
||||
/></a>
|
||||
</div>
|
||||
|
@ -1,123 +0,0 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { getConfig, setConfig, clearConfig, onConfigChange } from '../../lib/userConfig';
|
||||
import { getGitProviders } from '../../providers';
|
||||
|
||||
const resetButton = document.getElementById('reset');
|
||||
|
||||
const newDomainRow = () => {
|
||||
const template = document.getElementById('domain-row');
|
||||
if (template instanceof HTMLTemplateElement) {
|
||||
const row = template.content.firstElementChild.cloneNode(true);
|
||||
return row;
|
||||
}
|
||||
throw new Error('No row template found');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} row
|
||||
*/
|
||||
const domainToggles = (row) => {
|
||||
if (row.id === 'row-default') return;
|
||||
|
||||
const toggleRow = (allEnabled) => {
|
||||
const checkbox = row.querySelectorAll('.extEnabled').item(0);
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
checkbox.disabled = !allEnabled;
|
||||
checkbox.indeterminate = !allEnabled;
|
||||
}
|
||||
if (allEnabled) row.classList.remove('disabled');
|
||||
else row.classList.add('disabled');
|
||||
};
|
||||
|
||||
getConfig('extEnabled', 'default').then(toggleRow);
|
||||
onConfigChange('extEnabled', toggleRow, 'default');
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} row
|
||||
* @param {string} domain
|
||||
*/
|
||||
const fillRow = (row, domain) => {
|
||||
row.id = `row-${domain}`;
|
||||
|
||||
const title = row.getElementsByClassName('domain-name').item(0);
|
||||
title.appendChild(document.createTextNode(domain));
|
||||
|
||||
if (domain === 'default') {
|
||||
[...row.getElementsByClassName('default-option')].forEach((opt) => opt.remove());
|
||||
}
|
||||
|
||||
resetButton.addEventListener('click', () => {
|
||||
row.classList.add('brightDomain');
|
||||
setTimeout(() => row.classList.add('animated'), 0);
|
||||
setTimeout(() => row.classList.remove('brightDomain'), 0);
|
||||
setTimeout(() => row.classList.remove('animated'), 800);
|
||||
});
|
||||
|
||||
const wireConfig = (config, updateInput, updateConfig) => {
|
||||
const input = row.getElementsByClassName(config).item(0);
|
||||
|
||||
const populateInput = () => getConfig(config, domain, false).then(updateInput(input));
|
||||
|
||||
input.addEventListener('change', updateConfig(config));
|
||||
onConfigChange(config, updateInput(input), domain);
|
||||
onConfigChange(
|
||||
config,
|
||||
() => getConfig(config, domain, false).then(updateInput(input)),
|
||||
'default'
|
||||
);
|
||||
resetButton.addEventListener('click', () => clearConfig(config, domain).then(populateInput));
|
||||
|
||||
[...input.getElementsByClassName('default-option')].forEach((opt) => {
|
||||
input.addEventListener('focus', () => {
|
||||
opt.text = '(default)';
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
opt.text = '';
|
||||
});
|
||||
});
|
||||
|
||||
return populateInput();
|
||||
};
|
||||
|
||||
const updateSelect = (input) => (val) => {
|
||||
input.value = val ?? 'default';
|
||||
};
|
||||
const updateConfigFromSelect =
|
||||
(config) =>
|
||||
({ target: { value } }) =>
|
||||
!value || value === '(default)'
|
||||
? clearConfig(config, domain)
|
||||
: setConfig(config, value, domain);
|
||||
const wireSelect = (config) => wireConfig(config, updateSelect, updateConfigFromSelect);
|
||||
|
||||
const updateCheck = (input) => (val) => {
|
||||
input.checked = val ?? true;
|
||||
};
|
||||
const updateConfigFromCheck =
|
||||
(config) =>
|
||||
({ target: { checked } }) =>
|
||||
setConfig(config, checked, domain);
|
||||
const wireCheck = (config) => wireConfig(config, updateCheck, updateConfigFromCheck);
|
||||
|
||||
return Promise.all([wireSelect('iconSize'), wireSelect('iconPack'), wireCheck('extEnabled')])
|
||||
.then(() => domainToggles(row))
|
||||
.then(() => row);
|
||||
};
|
||||
|
||||
function getDomains() {
|
||||
return getGitProviders().then((providers) => [
|
||||
'default',
|
||||
...Object.values(providers)
|
||||
.map((p) => p.domains.map((d) => d.host))
|
||||
.flat(),
|
||||
]);
|
||||
}
|
||||
|
||||
const domainsDiv = document.getElementById('domains');
|
||||
|
||||
getDomains().then((domains) => {
|
||||
Promise.all(domains.map((d) => fillRow(newDomainRow(), d))).then((rows) =>
|
||||
rows.forEach((r) => domainsDiv.appendChild(r))
|
||||
);
|
||||
});
|
149
src/ui/options/options.ts
Normal file
149
src/ui/options/options.ts
Normal file
@ -0,0 +1,149 @@
|
||||
import {
|
||||
UserConfig,
|
||||
clearConfig,
|
||||
getConfig,
|
||||
onConfigChange,
|
||||
setConfig,
|
||||
} from '../../lib/user-config';
|
||||
import { getGitProviders } from '../../providers';
|
||||
|
||||
const resetButton = document.getElementById(
|
||||
'reset'
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
interface DomainRowElement extends HTMLElement {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const newDomainRow = (): ChildNode => {
|
||||
const template = document.getElementById('domain-row');
|
||||
if (template instanceof HTMLTemplateElement) {
|
||||
const row = template.content.firstElementChild?.cloneNode(true);
|
||||
if (!row) throw new Error('Row clone failed');
|
||||
return row as ChildNode;
|
||||
}
|
||||
throw new Error('No row template found');
|
||||
};
|
||||
|
||||
const domainToggles = (row: DomainRowElement): void => {
|
||||
if (row.id === 'row-default') return;
|
||||
|
||||
const toggleRow = (allEnabled: boolean): void => {
|
||||
const checkbox = row.querySelector(
|
||||
'.extEnabled'
|
||||
) as HTMLInputElement | null;
|
||||
if (checkbox) {
|
||||
checkbox.disabled = !allEnabled;
|
||||
checkbox.indeterminate = !allEnabled;
|
||||
}
|
||||
if (allEnabled) row.classList.remove('disabled');
|
||||
else row.classList.add('disabled');
|
||||
};
|
||||
|
||||
getConfig('extEnabled', 'default').then(toggleRow);
|
||||
onConfigChange('extEnabled', toggleRow, 'default');
|
||||
};
|
||||
|
||||
const fillRow = (
|
||||
rowElement: ChildNode,
|
||||
domain: string
|
||||
): Promise<DomainRowElement> => {
|
||||
const row = rowElement as DomainRowElement;
|
||||
row.id = `row-${domain}`;
|
||||
|
||||
const title = row.querySelector('.domain-name');
|
||||
if (title) title.appendChild(document.createTextNode(domain));
|
||||
|
||||
if (domain === 'default') {
|
||||
row.querySelectorAll('.default-option').forEach((opt) => opt.remove());
|
||||
}
|
||||
|
||||
resetButton?.addEventListener('click', () => {
|
||||
row.classList.add('brightDomain');
|
||||
setTimeout(() => row.classList.add('animated'), 0);
|
||||
setTimeout(() => row.classList.remove('brightDomain'), 0);
|
||||
setTimeout(() => row.classList.remove('animated'), 800);
|
||||
});
|
||||
|
||||
const wireConfig = <T>(
|
||||
configName: keyof UserConfig,
|
||||
updateInput: (input: HTMLElement) => (val: T) => void,
|
||||
updateConfig: (configName: keyof UserConfig) => (event: Event) => void
|
||||
): Promise<void> => {
|
||||
const input = row.querySelector(`.${configName}`) as HTMLElement;
|
||||
|
||||
const populateInput = (): Promise<void> =>
|
||||
getConfig(configName, domain, false).then(updateInput(input));
|
||||
|
||||
input.addEventListener('change', updateConfig(configName));
|
||||
onConfigChange(configName, updateInput(input), domain);
|
||||
onConfigChange(
|
||||
configName,
|
||||
() => getConfig(configName, domain, false).then(updateInput(input)),
|
||||
'default'
|
||||
);
|
||||
resetButton?.addEventListener('click', () =>
|
||||
clearConfig(configName, domain).then(populateInput)
|
||||
);
|
||||
|
||||
input.querySelectorAll('.default-option').forEach((opt) => {
|
||||
input.addEventListener('focus', () => {
|
||||
(opt as HTMLOptionElement).text = '(default)';
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
(opt as HTMLOptionElement).text = '';
|
||||
});
|
||||
});
|
||||
|
||||
return populateInput();
|
||||
};
|
||||
|
||||
const updateSelect = (input: HTMLElement) => (val?: string) => {
|
||||
(input as HTMLSelectElement).value = val ?? 'default';
|
||||
};
|
||||
const updateConfigFromSelect =
|
||||
(configName: keyof UserConfig) =>
|
||||
({ target }: Event) => {
|
||||
const value = (target as HTMLSelectElement).value;
|
||||
return !value || value === '(default)'
|
||||
? clearConfig(configName, domain)
|
||||
: setConfig(configName, value, domain);
|
||||
};
|
||||
const wireSelect = (configName: keyof UserConfig) =>
|
||||
wireConfig(configName, updateSelect, updateConfigFromSelect);
|
||||
|
||||
const updateCheck = (input: HTMLElement) => (val?: boolean) => {
|
||||
(input as HTMLInputElement).checked = val ?? true;
|
||||
};
|
||||
const updateConfigFromCheck =
|
||||
(configName: keyof UserConfig) =>
|
||||
({ target }: Event) => {
|
||||
const checked = (target as HTMLInputElement).checked;
|
||||
return setConfig(configName, checked, domain);
|
||||
};
|
||||
const wireCheck = (configName: keyof UserConfig) =>
|
||||
wireConfig(configName, updateCheck, updateConfigFromCheck);
|
||||
|
||||
return Promise.all([
|
||||
wireSelect('iconSize'),
|
||||
wireSelect('iconPack'),
|
||||
wireCheck('extEnabled'),
|
||||
])
|
||||
.then(() => domainToggles(row))
|
||||
.then(() => row);
|
||||
};
|
||||
|
||||
function getDomains(): Promise<string[]> {
|
||||
return getGitProviders().then((providers) => [
|
||||
'default',
|
||||
...Object.values(providers).flatMap((p) => p.domains.map((d) => d.host)),
|
||||
]);
|
||||
}
|
||||
|
||||
const domainsDiv = document.getElementById('domains') as HTMLDivElement;
|
||||
|
||||
getDomains().then((domains) => {
|
||||
Promise.all(domains.map((d) => fillRow(newDomainRow(), d))).then((rows) =>
|
||||
rows.forEach((r) => domainsDiv.appendChild(r))
|
||||
);
|
||||
});
|
@ -3,7 +3,7 @@ body {
|
||||
margin: 0;
|
||||
color: #1a202c;
|
||||
font-size: 16px;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
#content {
|
||||
|
@ -18,9 +18,10 @@
|
||||
</div>
|
||||
|
||||
<div id="request">
|
||||
We need access to the website to show the icons. If you click the button bellow the
|
||||
browser will ask you for permision in another popup, this one will close. If you allow the
|
||||
permission you will have to come back to finish the setup.
|
||||
We need access to the website to show the icons. If you click the
|
||||
button bellow the browser will ask you for permision in another popup,
|
||||
this one will close. If you allow the permission you will have to come
|
||||
back to finish the setup.
|
||||
|
||||
<button type="button" id="request-access" class="btn">
|
||||
<span>Allow access to website</span>
|
||||
@ -28,11 +29,13 @@
|
||||
</div>
|
||||
|
||||
<div id="not-supported">
|
||||
<span id="unsupported-domain"></span> is not supported by this extension
|
||||
<span id="unsupported-domain"></span> is not supported by this
|
||||
extension
|
||||
</div>
|
||||
|
||||
<div id="default-disabled-note">
|
||||
All icon replacements are disabled for all domains. To change this setting, go to the
|
||||
All icon replacements are disabled for all domains. To change this
|
||||
setting, go to the
|
||||
<a id="options-link">options page</a>
|
||||
</div>
|
||||
|
||||
@ -62,7 +65,10 @@
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path fill="currentColor" d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -86,7 +92,10 @@
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path fill="currentColor" d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,7 +114,10 @@
|
||||
focusable="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path fill="currentColor" d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -118,7 +130,7 @@
|
||||
|
||||
<div id="footer">
|
||||
<a
|
||||
href="https://github.com/Claudiohbsantos/github-material-icons-extension"
|
||||
href="https://github.com/material-extensions/material-icons-browser-addon"
|
||||
target="_blank"
|
||||
><img src="settings-popup.github-logo.svg"
|
||||
/></a>
|
||||
|
@ -1,218 +0,0 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
import { addCustomProvider } from '../../lib/custom-providers';
|
||||
import { getConfig, setConfig } from '../../lib/userConfig';
|
||||
import { addGitProvider, getGitProvider, providerConfig } from '../../providers';
|
||||
|
||||
const HOST_IS_NEW = 1;
|
||||
const HOST_NO_MATCH = 2;
|
||||
|
||||
const isPageSupported = (domain) => getGitProvider(domain);
|
||||
|
||||
function getCurrentTab() {
|
||||
const queryOptions = { active: true, currentWindow: true };
|
||||
|
||||
return Browser.tabs.query(queryOptions).then(([tab]) => tab);
|
||||
}
|
||||
|
||||
function registerControls(domain) {
|
||||
getConfig('iconSize', domain).then((size) => {
|
||||
document.getElementById('icon-size').value = size;
|
||||
});
|
||||
const updateIconSize = (event) => setConfig('iconSize', event.target.value, domain);
|
||||
document?.getElementById('icon-size')?.addEventListener('change', updateIconSize);
|
||||
|
||||
getConfig('iconPack', domain).then((pack) => {
|
||||
document.getElementById('icon-pack').value = pack;
|
||||
});
|
||||
const updateIconPack = (event) => setConfig('iconPack', event.target.value, domain);
|
||||
document?.getElementById('icon-pack')?.addEventListener('change', updateIconPack);
|
||||
|
||||
getConfig('extEnabled', domain).then((enabled) => {
|
||||
document.getElementById('enabled').checked = enabled;
|
||||
});
|
||||
const updateExtEnabled = (event) => setConfig('extEnabled', event.target.checked, domain);
|
||||
document?.getElementById('enabled')?.addEventListener('change', updateExtEnabled);
|
||||
|
||||
document
|
||||
.getElementById('options-btn')
|
||||
?.addEventListener('click', () => Browser.runtime.openOptionsPage());
|
||||
}
|
||||
|
||||
function setDomain(domain) {
|
||||
document.getElementById('domain-name').innerText = domain;
|
||||
}
|
||||
|
||||
function displayDomainSettings() {
|
||||
document.getElementById('domain-settings').style.display = 'block';
|
||||
}
|
||||
|
||||
function displayPageNotSupported(domain) {
|
||||
document.getElementById('unsupported-domain').innerText = domain;
|
||||
document.getElementById('not-supported').style.display = 'block';
|
||||
}
|
||||
|
||||
function askDomainAccess(tab) {
|
||||
document.getElementById('request').style.display = 'block';
|
||||
const clicked = () => {
|
||||
requestAccess(tab);
|
||||
// window.close();
|
||||
};
|
||||
document.getElementById('request-access').addEventListener('click', clicked);
|
||||
}
|
||||
|
||||
function displayCustomDomain(tab, domain, suggestedProvider) {
|
||||
document.getElementById('enable-wrapper').style.display = 'none';
|
||||
document.getElementById('icon-size-wrapper').style.display = 'none';
|
||||
document.getElementById('icon-pack-wrapper').style.display = 'none';
|
||||
|
||||
const btn = document.getElementById('add-provider');
|
||||
const providerEl = document.getElementById('provider-wrapper');
|
||||
|
||||
btn.style.display = 'block';
|
||||
providerEl.style.display = 'block';
|
||||
|
||||
const select = providerEl.querySelector('#provider');
|
||||
|
||||
for (const provider of Object.values(providerConfig)) {
|
||||
if (!provider.isCustom && provider.canSelfHost) {
|
||||
const selected = provider.name === suggestedProvider;
|
||||
const opt = new Option(provider.name, provider.name, selected, selected);
|
||||
|
||||
select.append(opt);
|
||||
}
|
||||
}
|
||||
|
||||
const addProvider = () => {
|
||||
addCustomProvider(domain, select.value).then(() => {
|
||||
addGitProvider(domain, select.value);
|
||||
|
||||
const cmd = {
|
||||
cmd: 'init',
|
||||
};
|
||||
|
||||
Browser.tabs.sendMessage(tab.id, cmd);
|
||||
|
||||
// reload the popup to show the settings.
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
btn.addEventListener('click', addProvider);
|
||||
}
|
||||
|
||||
function displayAllDisabledNote() {
|
||||
getConfig('extEnabled', 'default').then((enabled) => {
|
||||
if (enabled) return;
|
||||
document.getElementById('default-disabled-note').style.display = 'block';
|
||||
document.getElementById('domain-settings').style.display = 'none';
|
||||
document
|
||||
.getElementById('options-link')
|
||||
?.addEventListener('click', () => Browser.runtime.openOptionsPage());
|
||||
});
|
||||
}
|
||||
|
||||
function guessProvider(tab) {
|
||||
const possibilities = {};
|
||||
|
||||
for (const provider of Object.values(providerConfig)) {
|
||||
if (!provider.isCustom && provider.canSelfHost && provider.selectors.detect) {
|
||||
possibilities[provider.name] = provider.selectors.detect;
|
||||
}
|
||||
}
|
||||
|
||||
const cmd = {
|
||||
cmd: 'guessProvider',
|
||||
args: [possibilities],
|
||||
};
|
||||
|
||||
return Browser.tabs.sendMessage(tab.id, cmd).then((match) => {
|
||||
if (match === null) {
|
||||
return HOST_NO_MATCH;
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
function checkAccess(tab) {
|
||||
const { host } = new URL(tab.url);
|
||||
|
||||
const perm = {
|
||||
permissions: ['activeTab'],
|
||||
origins: [`*://${host}/*`],
|
||||
};
|
||||
|
||||
return Browser.permissions.contains(perm).then((r) => {
|
||||
if (r) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
return HOST_IS_NEW;
|
||||
});
|
||||
}
|
||||
|
||||
function requestAccess(tab) {
|
||||
const { host } = new URL(tab.url);
|
||||
|
||||
return Browser.runtime.sendMessage({
|
||||
event: 'request-access',
|
||||
data: {
|
||||
tabId: tab.id,
|
||||
url: tab.url,
|
||||
host,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function doGuessProvider(tab, domain) {
|
||||
return guessProvider(tab).then((match) => {
|
||||
if (match !== HOST_NO_MATCH) {
|
||||
registerControls(domain);
|
||||
displayDomainSettings();
|
||||
|
||||
return displayCustomDomain(tab, domain, match);
|
||||
}
|
||||
|
||||
return displayPageNotSupported(domain);
|
||||
});
|
||||
}
|
||||
|
||||
function isFirefox() {
|
||||
return typeof browser !== 'undefined' && typeof chrome !== 'undefined';
|
||||
}
|
||||
|
||||
function init(tab) {
|
||||
const domain = new URL(tab.url).host;
|
||||
|
||||
setDomain(domain);
|
||||
|
||||
isPageSupported(domain).then((supported) => {
|
||||
if (!supported) {
|
||||
// we are in some internal browser page, not supported.
|
||||
if (!tab.url.startsWith('http')) {
|
||||
return displayPageNotSupported(domain);
|
||||
}
|
||||
|
||||
// overwrite for firefox browser, currently does not support
|
||||
// asking for permissions from background, so it will run
|
||||
// on all pages.
|
||||
if (isFirefox()) {
|
||||
return doGuessProvider(tab, domain);
|
||||
}
|
||||
|
||||
return checkAccess(tab).then((access) => {
|
||||
if (access === HOST_IS_NEW) {
|
||||
return askDomainAccess(tab);
|
||||
}
|
||||
|
||||
return doGuessProvider(tab, domain);
|
||||
});
|
||||
}
|
||||
|
||||
registerControls(domain);
|
||||
displayDomainSettings();
|
||||
displayAllDisabledNote();
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentTab().then(init);
|
250
src/ui/popup/settings-popup.ts
Normal file
250
src/ui/popup/settings-popup.ts
Normal file
@ -0,0 +1,250 @@
|
||||
import Browser from 'webextension-polyfill';
|
||||
import { addCustomProvider } from '../../lib/custom-providers';
|
||||
import { getConfig, setConfig } from '../../lib/user-config';
|
||||
import {
|
||||
addGitProvider,
|
||||
getGitProvider,
|
||||
providerConfig,
|
||||
} from '../../providers';
|
||||
|
||||
const HOST_IS_NEW = 1;
|
||||
const HOST_NO_MATCH = 2;
|
||||
|
||||
const isPageSupported = (domain: string) => getGitProvider(domain);
|
||||
|
||||
function getCurrentTab() {
|
||||
const queryOptions = { active: true, currentWindow: true };
|
||||
|
||||
return Browser.tabs.query(queryOptions).then(([tab]) => tab);
|
||||
}
|
||||
|
||||
function registerControls(domain: string) {
|
||||
getConfig('iconSize', domain).then((size) => {
|
||||
getElementByIdOrThrow<HTMLInputElement>('icon-size').value = size;
|
||||
});
|
||||
const updateIconSize = (event: Event) =>
|
||||
setConfig('iconSize', (event.target as HTMLInputElement).value, domain);
|
||||
document
|
||||
?.getElementById('icon-size')
|
||||
?.addEventListener('change', updateIconSize);
|
||||
|
||||
getConfig('iconPack', domain).then((pack) => {
|
||||
getElementByIdOrThrow<HTMLInputElement>('icon-pack').value = pack;
|
||||
});
|
||||
const updateIconPack = (event: Event) =>
|
||||
setConfig('iconPack', (event.target as HTMLInputElement).value, domain);
|
||||
document
|
||||
?.getElementById('icon-pack')
|
||||
?.addEventListener('change', updateIconPack);
|
||||
|
||||
getConfig('extEnabled', domain).then((enabled) => {
|
||||
getElementByIdOrThrow<HTMLInputElement>('enabled').checked = enabled;
|
||||
});
|
||||
const updateExtEnabled = (event: Event) =>
|
||||
setConfig('extEnabled', (event.target as HTMLInputElement).checked, domain);
|
||||
document
|
||||
?.getElementById('enabled')
|
||||
?.addEventListener('change', updateExtEnabled);
|
||||
|
||||
document
|
||||
.getElementById('options-btn')
|
||||
?.addEventListener('click', () => Browser.runtime.openOptionsPage());
|
||||
}
|
||||
|
||||
function setDomain(domain: string) {
|
||||
getElementByIdOrThrow('domain-name').innerText = domain;
|
||||
}
|
||||
|
||||
function displayDomainSettings() {
|
||||
getElementByIdOrThrow('domain-settings').style.display = 'block';
|
||||
}
|
||||
|
||||
function displayPageNotSupported(domain: string) {
|
||||
getElementByIdOrThrow('unsupported-domain').innerText = domain;
|
||||
getElementByIdOrThrow('not-supported').style.display = 'block';
|
||||
}
|
||||
|
||||
function askDomainAccess(tab: Browser.Tabs.Tab) {
|
||||
getElementByIdOrThrow('request').style.display = 'block';
|
||||
const clicked = () => {
|
||||
requestAccess(tab);
|
||||
};
|
||||
getElementByIdOrThrow('request-access').addEventListener('click', clicked);
|
||||
}
|
||||
|
||||
function displayCustomDomain(
|
||||
tab: Browser.Tabs.Tab,
|
||||
domain: string,
|
||||
suggestedProvider: string
|
||||
) {
|
||||
getElementByIdOrThrow('enable-wrapper').style.display = 'none';
|
||||
getElementByIdOrThrow('icon-size-wrapper').style.display = 'none';
|
||||
getElementByIdOrThrow('icon-pack-wrapper').style.display = 'none';
|
||||
|
||||
const btn = getElementByIdOrThrow('add-provider');
|
||||
const providerEl = getElementByIdOrThrow('provider-wrapper');
|
||||
|
||||
btn.style.display = 'block';
|
||||
providerEl.style.display = 'block';
|
||||
|
||||
const select = providerEl.querySelector(
|
||||
'#provider'
|
||||
) as HTMLInputElement | null;
|
||||
|
||||
for (const provider of Object.values(providerConfig)) {
|
||||
if (!provider.isCustom && provider.canSelfHost) {
|
||||
const selected = provider.name === suggestedProvider;
|
||||
const opt = new Option(provider.name, provider.name, selected, selected);
|
||||
|
||||
select?.append(opt);
|
||||
}
|
||||
}
|
||||
|
||||
const addProvider = () => {
|
||||
if (!select) return;
|
||||
addCustomProvider(domain, select.value).then(() => {
|
||||
addGitProvider(domain, select.value);
|
||||
|
||||
const cmd = {
|
||||
cmd: 'init',
|
||||
};
|
||||
|
||||
Browser.tabs.sendMessage(tab.id ?? 0, cmd);
|
||||
|
||||
// reload the popup to show the settings.
|
||||
window.location.reload();
|
||||
});
|
||||
};
|
||||
|
||||
btn.addEventListener('click', addProvider);
|
||||
}
|
||||
|
||||
function displayAllDisabledNote() {
|
||||
getConfig('extEnabled', 'default').then((enabled) => {
|
||||
if (enabled) return;
|
||||
getElementByIdOrThrow('default-disabled-note').style.display = 'block';
|
||||
getElementByIdOrThrow('domain-settings').style.display = 'none';
|
||||
document
|
||||
.getElementById('options-link')
|
||||
?.addEventListener('click', () => Browser.runtime.openOptionsPage());
|
||||
});
|
||||
}
|
||||
|
||||
function guessProvider(tab: Browser.Tabs.Tab) {
|
||||
const possibilities: Record<string, string> = {};
|
||||
|
||||
for (const provider of Object.values(providerConfig)) {
|
||||
if (
|
||||
!provider.isCustom &&
|
||||
provider.canSelfHost &&
|
||||
provider.selectors.detect
|
||||
) {
|
||||
possibilities[provider.name] = provider.selectors.detect;
|
||||
}
|
||||
}
|
||||
|
||||
const cmd = {
|
||||
cmd: 'guessProvider',
|
||||
args: [possibilities],
|
||||
};
|
||||
|
||||
return Browser.tabs.sendMessage(tab.id ?? 0, cmd).then((match) => {
|
||||
if (match === null) {
|
||||
return HOST_NO_MATCH;
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
}
|
||||
|
||||
function getElementByIdOrThrow<T = HTMLElement>(id: string): NonNullable<T> {
|
||||
const el = document.getElementById(id) as T | null;
|
||||
if (!el) {
|
||||
throw new Error(`Element with id ${id} not found`);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
function checkAccess(tab: Browser.Tabs.Tab) {
|
||||
const { host } = new URL(tab.url ?? '');
|
||||
|
||||
const perm = {
|
||||
permissions: ['activeTab'],
|
||||
origins: [`*://${host}/*`],
|
||||
};
|
||||
|
||||
return Browser.permissions.contains(perm).then((r) => {
|
||||
if (r) {
|
||||
return tab;
|
||||
}
|
||||
|
||||
return HOST_IS_NEW;
|
||||
});
|
||||
}
|
||||
|
||||
function requestAccess(tab: Browser.Tabs.Tab) {
|
||||
const { host } = new URL(tab.url ?? '');
|
||||
|
||||
return Browser.runtime.sendMessage({
|
||||
event: 'request-access',
|
||||
data: {
|
||||
tabId: tab.id,
|
||||
url: tab.url,
|
||||
host,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function doGuessProvider(tab: Browser.Tabs.Tab, domain: string) {
|
||||
return guessProvider(tab).then((match) => {
|
||||
if (match !== HOST_NO_MATCH) {
|
||||
registerControls(domain);
|
||||
displayDomainSettings();
|
||||
|
||||
return displayCustomDomain(tab, domain, match);
|
||||
}
|
||||
|
||||
return displayPageNotSupported(domain);
|
||||
});
|
||||
}
|
||||
|
||||
function isFirefox() {
|
||||
return navigator.userAgent.toLowerCase().includes('firefox');
|
||||
}
|
||||
|
||||
function init(tab: Browser.Tabs.Tab) {
|
||||
const domain = new URL(tab.url ?? '').host;
|
||||
|
||||
setDomain(domain);
|
||||
|
||||
isPageSupported(domain).then((supported) => {
|
||||
if (!supported) {
|
||||
// we are in some internal browser page, not supported.
|
||||
if (tab.url && !tab.url.startsWith('http')) {
|
||||
return displayPageNotSupported(domain);
|
||||
}
|
||||
|
||||
// overwrite for firefox browser, currently does not support
|
||||
// asking for permissions from background, so it will run
|
||||
// on all pages.
|
||||
if (isFirefox()) {
|
||||
return doGuessProvider(tab, domain);
|
||||
}
|
||||
|
||||
return checkAccess(tab).then((access) => {
|
||||
if (access === HOST_IS_NEW) {
|
||||
return askDomainAccess(tab);
|
||||
}
|
||||
|
||||
return doGuessProvider(tab, domain);
|
||||
});
|
||||
}
|
||||
|
||||
registerControls(domain);
|
||||
displayDomainSettings();
|
||||
displayAllDisabledNote();
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentTab().then(init);
|
@ -1,10 +0,0 @@
|
||||
const { extendDefaultPlugins } = require('svgo');
|
||||
|
||||
module.exports = {
|
||||
plugins: extendDefaultPlugins([
|
||||
{
|
||||
name: 'removeViewBox',
|
||||
active: false,
|
||||
},
|
||||
]),
|
||||
};
|
13
tsconfig.json
Normal file
13
tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["es2022", "dom"]
|
||||
},
|
||||
"include": ["./src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
@ -1 +0,0 @@
|
||||
8b32ebe2b03442def2e611cba34ad0e9d8866fd3
|
@ -1 +0,0 @@
|
||||
5.3.0
|
Loading…
x
Reference in New Issue
Block a user