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:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
type:
|
versionChange:
|
||||||
description: major|minor|patch - defaults to patch
|
type: choice
|
||||||
|
description: Select the version change
|
||||||
required: true
|
required: true
|
||||||
default: patch
|
default: 'patch'
|
||||||
|
options:
|
||||||
|
- major
|
||||||
|
- minor
|
||||||
|
- patch
|
||||||
onlyUpload:
|
onlyUpload:
|
||||||
description: chrome|firefox|both|not|none - Only upload to store(s) without bumping version or releasing
|
description: chrome|firefox|both|not|none - Only upload to store(s) without bumping version or releasing
|
||||||
required: true
|
required: true
|
||||||
default: not
|
default: not
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
get-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '16.13.0'
|
node-version: "20"
|
||||||
|
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@ -35,20 +49,19 @@ jobs:
|
|||||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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
|
- name: Get package version
|
||||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||||
id: package-version
|
run: echo "package_version=v$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||||
run: echo ::set-output name=package_version::v$(jq -r .version package.json)
|
|
||||||
|
|
||||||
- name: Commit updated files
|
- name: Commit updated files
|
||||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||||
uses: EndBug/add-and-commit@v7
|
uses: EndBug/add-and-commit@v7
|
||||||
with:
|
with:
|
||||||
add: '.'
|
add: "."
|
||||||
message: '[auto] release ${{steps.package-version.outputs.package_version}}'
|
message: "[auto] release ${{env.package_version}}"
|
||||||
tag: '${{steps.package-version.outputs.package_version}}'
|
tag: "${{env.package_version}}"
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
if: ${{ github.event.inputs.onlyUpload == 'not' || github.event.inputs.onlyUpload == 'none' }}
|
||||||
@ -56,15 +69,15 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tag_name: '${{steps.package-version.outputs.package_version}}'
|
tag_name: "${{env.package_version}}"
|
||||||
files: '*.zip'
|
files: "*.zip"
|
||||||
|
|
||||||
- name: Upload to chrome store
|
- name: Upload to chrome store
|
||||||
if: ${{ github.event.inputs.onlyUpload != 'firefox' && github.event.inputs.onlyUpload != 'none' }}
|
if: ${{ github.event.inputs.onlyUpload != 'firefox' && github.event.inputs.onlyUpload != 'none' }}
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: trmcnvn/chrome-addon@v2
|
uses: trmcnvn/chrome-addon@v2
|
||||||
with:
|
with:
|
||||||
extension: bggfcpfjbdkhfhfmkjpbhnkhnpjjeomc
|
extension: hopghfcljkdgmajlhdfpgpcemcfhbili
|
||||||
zip: github-material-icons-chrome-extension.zip
|
zip: github-material-icons-chrome-extension.zip
|
||||||
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
client-id: ${{ secrets.CHROME_CLIENT_ID }}
|
||||||
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
client-secret: ${{ secrets.CHROME_CLIENT_SECRET }}
|
||||||
@ -75,7 +88,7 @@ jobs:
|
|||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: wdzeng/edge-addon@v1
|
uses: wdzeng/edge-addon@v1
|
||||||
with:
|
with:
|
||||||
product-id: d7692295-d84f-4bf5-9447-3cbb6ae29517
|
product-id: fmnacigfpppckhpaafbjdhljbjjclkkj
|
||||||
zip-path: github-material-icons-edge-extension.zip
|
zip-path: github-material-icons-edge-extension.zip
|
||||||
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
client-id: ${{ secrets.EDGE_CLIENT_ID }}
|
||||||
client-secret: ${{ secrets.EDGE_CLIENT_SECRET }}
|
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
|
name: Auto Update when upstream releases
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 3 * * *'
|
- cron: "0 3 * * *"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
get-version:
|
get-version:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '16.13.0'
|
node-version: "20"
|
||||||
|
|
||||||
- name: Fetch release version
|
- name: Fetch release version
|
||||||
id: upstream
|
id: upstream
|
||||||
run: |
|
run: |
|
||||||
echo ::set-output name=release_tag::$(curl "https://raw.githubusercontent.com/PKief/vscode-material-icon-theme/main/package.json" \
|
release_tag=$(npm view material-icon-theme version)
|
||||||
| jq -r .version)
|
current_tag=$(npm list material-icon-theme --depth=0 | grep 'material-icon-theme@' | cut -d '@' -f 2)
|
||||||
echo ::set-output name=current_tag::$(<upstream.version)
|
echo "release_tag=$release_tag" >> $GITHUB_ENV
|
||||||
|
echo "current_tag=$current_tag" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Attempt update
|
- name: Attempt update
|
||||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
if: env.release_tag != env.current_tag
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npm ci && npm run update
|
run: npm ci && npm run update
|
||||||
|
|
||||||
- name: Get package version
|
- name: Get package version
|
||||||
id: package-version
|
if: env.release_tag != env.current_tag
|
||||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
run: |
|
||||||
run: echo ::set-output name=package_version::v$(jq -r .version package.json)
|
echo "package_version=v$(jq -r .version package.json)" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Commit updated files
|
- name: Commit updated files
|
||||||
if: steps.upstream.outputs.release_tag != steps.upstream.outputs.current_tag
|
if: env.release_tag != env.current_tag
|
||||||
uses: EndBug/add-and-commit@v7
|
uses: EndBug/add-and-commit@v9
|
||||||
with:
|
with:
|
||||||
add: '.'
|
add: "."
|
||||||
message: '[auto] update to upstream ${{steps.upstream.outputs.release_tag}}'
|
message: "[auto] update to upstream ${{env.release_tag}}"
|
||||||
tag: '${{steps.package-version.outputs.package_version}}'
|
tag: "${{env.package_version}}"
|
||||||
|
|
||||||
- name: Release
|
- 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
|
uses: softprops/action-gh-release@v1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
with:
|
with:
|
||||||
tag_name: '${{steps.package-version.outputs.package_version}}'
|
tag_name: "${{env.package_version}}"
|
||||||
body: Built with icons from VSCode Material Icon Theme ${{steps.upstream.outputs.release_tag}}
|
body: Built with icons from VSCode Material Icon Theme ${{env.release_tag}}
|
||||||
files: '*.zip'
|
files: "*.zip"
|
||||||
|
|
||||||
- name: Upload to chrome store
|
- 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
|
continue-on-error: true
|
||||||
uses: trmcnvn/chrome-addon@v2
|
uses: trmcnvn/chrome-addon@v2
|
||||||
with:
|
with:
|
||||||
@ -63,7 +73,7 @@ jobs:
|
|||||||
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }}
|
||||||
|
|
||||||
- name: Upload to edge store
|
- 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
|
continue-on-error: true
|
||||||
uses: wdzeng/edge-addon@v1
|
uses: wdzeng/edge-addon@v1
|
||||||
with:
|
with:
|
||||||
@ -74,6 +84,6 @@ jobs:
|
|||||||
access-token-url: ${{ secrets.EDGE_ACCESS_TOKEN_URL }}
|
access-token-url: ${{ secrets.EDGE_ACCESS_TOKEN_URL }}
|
||||||
|
|
||||||
- name: Upload to firefox store
|
- 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
|
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 }}
|
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/
|
dist/
|
||||||
temp/
|
out/
|
||||||
.vscode/
|
|
||||||
github-material-icons-chrome-extension.zip
|
github-material-icons-chrome-extension.zip
|
||||||
github-material-icons-firefox-extension.zip
|
github-material-icons-firefox-extension.zip
|
||||||
github-material-icons-edge-extension.zip
|
github-material-icons-edge-extension.zip
|
||||||
src/icon-cache.js
|
|
||||||
src/icon-map.json
|
|
||||||
src/icon-list.json
|
src/icon-list.json
|
||||||
src/language-map.json
|
src/language-map.json
|
||||||
svg/
|
svg/
|
||||||
@ -15,8 +12,6 @@ data/
|
|||||||
node_modules
|
node_modules
|
||||||
*.log
|
*.log
|
||||||
.idea
|
.idea
|
||||||
.cache/
|
|
||||||
.vscode/
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
.eslintcache
|
||||||
Thumbs.db
|
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">
|
<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>
|
<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>
|
</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
|
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",
|
"name": "material-icons-browser-addon",
|
||||||
"version": "1.8.15",
|
"version": "1.8.15",
|
||||||
"description": "Browser Addon that enhances file browsers of version controls with material icons.",
|
"description": "Browser Addon that enhances file browsers of version controls with material icons.",
|
||||||
"main": "src/main.js",
|
"main": "src/main.ts",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Philipp Kief",
|
"name": "Material Extensions",
|
||||||
"email": "philipp.kief@gmx.de",
|
"url": "https://github.com/material-extensions"
|
||||||
"url": "https://pkief.com"
|
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/material-extensions/material-icons-browser-addon#readme",
|
"homepage": "https://github.com/material-extensions/material-icons-browser-addon#readme",
|
||||||
@ -17,62 +16,49 @@
|
|||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/material-extensions/material-icons-browser-addon/issues"
|
"url": "https://github.com/material-extensions/material-icons-browser-addon/issues"
|
||||||
},
|
},
|
||||||
"engines": {
|
|
||||||
"node": "^16.0.0",
|
|
||||||
"npm": "^8.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"material-icon-theme": "latest",
|
||||||
"selector-observer": "2.1.6",
|
"selector-observer": "2.1.6",
|
||||||
"webextension-polyfill": "0.11.0"
|
"webextension-polyfill": "0.12.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/core": "3.5.1",
|
"@octokit/core": "3.5.1",
|
||||||
"compare-versions": "3.6.0",
|
"@types/fs-extra": "11.0.4",
|
||||||
"eslint": "8.18.0",
|
"@types/json-stable-stringify": "1.0.36",
|
||||||
"eslint-config-airbnb-base": "15.0.0",
|
"@types/webextension-polyfill": "0.10.7",
|
||||||
"eslint-config-prettier": "8.5.0",
|
"esbuild": "0.21.5",
|
||||||
"eslint-plugin-import": "2.26.0",
|
"fs-extra": "11.2.0",
|
||||||
"eslint-plugin-jsdoc": "39.3.3",
|
"husky": "9.0.11",
|
||||||
"fs-extra": "10.0.0",
|
"json-stable-stringify": "1.1.1",
|
||||||
"husky": "8.0.1",
|
"lint-staged": "15.2.7",
|
||||||
"json-stable-stringify": "1.0.1",
|
|
||||||
"lint-staged": "13.0.3",
|
|
||||||
"node-fetch": "2.6.7",
|
|
||||||
"npm-run-all": "4.1.5",
|
"npm-run-all": "4.1.5",
|
||||||
"parcel-bundler": "1.12.5",
|
"rimraf": "5.0.7",
|
||||||
"prettier": "2.7.1",
|
"sharp": "0.33.4",
|
||||||
"rimraf": "3.0.2",
|
"ts-node": "10.9.2",
|
||||||
"sharp": "0.28.3",
|
"typescript": "5.5.2",
|
||||||
"simple-git": "2.40.0",
|
"web-ext": "8.2.0"
|
||||||
"svgo": "2.3.1",
|
|
||||||
"web-ext": "7.1.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "rimraf *.zip ./dist",
|
"prebuild": "rimraf *.zip ./dist",
|
||||||
"build": "run-s build-ext-dependencies build-languages build-src bundle",
|
"build": "run-s build-languages build-src bundle",
|
||||||
"build-ext-dependencies": "node ./scripts/build-dependencies.js",
|
"build-languages": "ts-node ./scripts/build-languages.ts",
|
||||||
"build-languages": "node ./scripts/build-languages.js",
|
"build-src": "ts-node ./scripts/build-src.ts",
|
||||||
"build-src": "node ./scripts/build-src.js",
|
"rebuild-logos": "ts-node ./scripts/build-icons.ts",
|
||||||
"rebuild-logos": "node ./scripts/build-icons.js",
|
|
||||||
"bundle": "run-p bundle-edge bundle-chrome bundle-firefox",
|
"bundle": "run-p bundle-edge bundle-chrome bundle-firefox",
|
||||||
"bundle-edge": "zip -r -j github-material-icons-edge-extension.zip dist/chrome-edge",
|
"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-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",
|
"bundle-firefox": "web-ext -s ./dist/firefox/ -n github-material-icons-firefox-extension.zip -a . build --overwrite-dest",
|
||||||
"parcel": "parcel build ./src/main.js",
|
"update-manifest-version": "ts-node ./scripts/update-manifest-version.ts",
|
||||||
"parcel-watch": "parcel watch ./src/main.js",
|
"update-upstream-version": "ts-node ./scripts/update-upstream-version.ts",
|
||||||
"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-package-version": "npm version --no-git-tag-version",
|
"update-package-version": "npm version --no-git-tag-version",
|
||||||
"update": "run-s update-upstream-version \"update-package-version patch\" update-manifest-version build",
|
"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 --",
|
"release": "run-s \"update-package-version {1}\" update-manifest-version build --",
|
||||||
"lint": "eslint .",
|
"lint": "npx @biomejs/biome check --write ./src",
|
||||||
"lint-fix": "eslint --fix .",
|
"format": "npx @biomejs/biome format --write ./src",
|
||||||
"format": "prettier --write --ignore-unknown .",
|
"prepare": "husky"
|
||||||
"prepare": "husky install"
|
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.js": "eslint --fix",
|
"*.ts": "npm run lint",
|
||||||
"*": "prettier --write --ignore-unknown"
|
"*": "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';
|
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') {
|
if (message.event === 'request-access') {
|
||||||
const perm = {
|
const perm: Browser.Permissions.Permissions = {
|
||||||
permissions: ['activeTab'],
|
permissions: ['activeTab'],
|
||||||
origins: [`*://${message.data.host}/*`],
|
origins: [`*://${message.data.host}/*`],
|
||||||
};
|
};
|
||||||
|
|
||||||
Browser.permissions.request(perm).then((granted) => {
|
Browser.permissions.request(perm).then((granted: boolean) => {
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
return;
|
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);
|
document.body.setAttribute(`data-material-icons-extension-size`, iconSize);
|
||||||
|
|
||||||
export const initIconSizes = () => {
|
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 */
|
/** The name of the class used to hide the pseudo element `:before` on Azure */
|
||||||
const HIDE_PSEUDO_CLASS = 'material-icons-exension-hide-pseudo';
|
const HIDE_PSEUDO_CLASS = 'material-icons-exension-hide-pseudo';
|
||||||
|
|
||||||
export default function azure() {
|
export default function azure(): Provider {
|
||||||
return {
|
return {
|
||||||
name: 'azure',
|
name: 'azure',
|
||||||
domains: [
|
domains: [
|
||||||
@ -25,11 +27,13 @@ export default function azure() {
|
|||||||
canSelfHost: false,
|
canSelfHost: false,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
getIsLightTheme: () =>
|
getIsLightTheme: () =>
|
||||||
document.defaultView.getComputedStyle(document.body).getPropertyValue('color') ===
|
document.defaultView
|
||||||
'rgba(0, 0, 0, 0.9)', // TODO: There is probably a better way to determine whether Azure is in light mode
|
?.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'),
|
getIsDirectory: ({ icon }) => icon.classList.contains('repos-folder-icon'),
|
||||||
getIsSubmodule: () => false, // There appears to be no way to tell if a folder is a submodule
|
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) => {
|
replaceIcon: (svgEl, newSVG) => {
|
||||||
newSVG.style.display = 'inline-flex';
|
newSVG.style.display = 'inline-flex';
|
||||||
newSVG.style.height = '1rem';
|
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,
|
// Instead of replacing the child icon, add the new icon as a child,
|
||||||
// otherwise Azure DevOps crashes when you navigate through the repository
|
// otherwise Azure DevOps crashes when you navigate through the repository
|
||||||
if (svgEl.hasChildNodes()) {
|
if (svgEl.hasChildNodes() && svgEl.firstChild !== null) {
|
||||||
svgEl.replaceChild(newSVG, svgEl.firstChild);
|
svgEl.replaceChild(newSVG, svgEl.firstChild);
|
||||||
} else {
|
} else {
|
||||||
svgEl.appendChild(newSVG);
|
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
|
// 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
|
// 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.
|
// 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
|
// Check whether the mutation was made by this extension
|
||||||
// this is determined by whether there is an image node added to the dom
|
// this is determined by whether there is an image node added to the dom
|
||||||
const isExtensionMutation = mutationsList.some((mutation) =>
|
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
|
// 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);
|
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 {
|
return {
|
||||||
name: 'bitbucket',
|
name: 'bitbucket',
|
||||||
domains: [
|
domains: [
|
||||||
@ -18,8 +20,12 @@ export default function bitbucket() {
|
|||||||
canSelfHost: true,
|
canSelfHost: true,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
getIsLightTheme: () => true, // No dark mode available for bitbucket currently
|
getIsLightTheme: () => true, // No dark mode available for bitbucket currently
|
||||||
getIsDirectory: ({ icon }) => icon.parentNode?.getAttribute('aria-label') === 'Directory,',
|
getIsDirectory: ({ icon }) =>
|
||||||
getIsSubmodule: ({ icon }) => icon.parentNode?.getAttribute('aria-label') === 'Submodule,',
|
(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
|
getIsSymlink: () => false, // There appears to be no way to determine this for bitbucket
|
||||||
replaceIcon: (svgEl, newSVG) => {
|
replaceIcon: (svgEl, newSVG) => {
|
||||||
newSVG.style.overflow = 'hidden';
|
newSVG.style.overflow = 'hidden';
|
||||||
@ -34,10 +40,10 @@ export default function bitbucket() {
|
|||||||
(attr) =>
|
(attr) =>
|
||||||
attr !== 'src' &&
|
attr !== 'src' &&
|
||||||
!/^data-material-icons-extension/.test(attr) &&
|
!/^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: () => {},
|
onAdd: () => {},
|
||||||
};
|
};
|
@ -1,4 +1,6 @@
|
|||||||
export default function gitea() {
|
import { Provider } from '../models';
|
||||||
|
|
||||||
|
export default function gitea(): Provider {
|
||||||
return {
|
return {
|
||||||
name: 'gitea',
|
name: 'gitea',
|
||||||
domains: [
|
domains: [
|
||||||
@ -17,9 +19,12 @@ export default function gitea() {
|
|||||||
canSelfHost: true,
|
canSelfHost: true,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
getIsLightTheme: () => false,
|
getIsLightTheme: () => false,
|
||||||
getIsDirectory: ({ icon }) => icon.classList.contains('octicon-file-directory-fill'),
|
getIsDirectory: ({ icon }) =>
|
||||||
getIsSubmodule: ({ icon }) => icon.classList.contains('octicon-file-submodule'),
|
icon.classList.contains('octicon-file-directory-fill'),
|
||||||
getIsSymlink: ({ icon }) => icon.classList.contains('octicon-file-symlink-file'),
|
getIsSubmodule: ({ icon }) =>
|
||||||
|
icon.classList.contains('octicon-file-submodule'),
|
||||||
|
getIsSymlink: ({ icon }) =>
|
||||||
|
icon.classList.contains('octicon-file-symlink-file'),
|
||||||
replaceIcon: (svgEl, newSVG) => {
|
replaceIcon: (svgEl, newSVG) => {
|
||||||
svgEl
|
svgEl
|
||||||
.getAttributeNames()
|
.getAttributeNames()
|
||||||
@ -27,10 +32,10 @@ export default function gitea() {
|
|||||||
(attr) =>
|
(attr) =>
|
||||||
attr !== 'src' &&
|
attr !== 'src' &&
|
||||||
!/^data-material-icons-extension/.test(attr) &&
|
!/^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: () => {},
|
onAdd: () => {},
|
||||||
};
|
};
|
@ -1,4 +1,6 @@
|
|||||||
export default function gitee() {
|
import { Provider } from '../models';
|
||||||
|
|
||||||
|
export default function gitee(): Provider {
|
||||||
return {
|
return {
|
||||||
name: 'gitee',
|
name: 'gitee',
|
||||||
domains: [
|
domains: [
|
||||||
@ -11,7 +13,8 @@ export default function gitee() {
|
|||||||
// File list row, README header, file view header
|
// File list row, README header, file view header
|
||||||
row: '#git-project-content .tree-content .row.tree-item, .file_title, .blob-description',
|
row: '#git-project-content .tree-content .row.tree-item, .file_title, .blob-description',
|
||||||
// File name table cell, Submodule name table cell, file view header
|
// 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
|
// The iconfont icon not including the delete button icon in the file view header
|
||||||
icon: 'i.iconfont:not(.icon-delete)',
|
icon: 'i.iconfont:not(.icon-delete)',
|
||||||
// Element by which to detect if the tested domain is gitee.
|
// Element by which to detect if the tested domain is gitee.
|
||||||
@ -30,13 +33,13 @@ export default function gitee() {
|
|||||||
(attr) =>
|
(attr) =>
|
||||||
attr !== 'src' &&
|
attr !== 'src' &&
|
||||||
!/^data-material-icons-extension/.test(attr) &&
|
!/^data-material-icons-extension/.test(attr) &&
|
||||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
newSVG.style.height = '28px';
|
newSVG.style.height = '28px';
|
||||||
newSVG.style.width = '18px';
|
newSVG.style.width = '18px';
|
||||||
|
|
||||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||||
},
|
},
|
||||||
onAdd: () => {},
|
onAdd: () => {},
|
||||||
};
|
};
|
@ -1,4 +1,6 @@
|
|||||||
export default function github() {
|
import { Provider } from '../models';
|
||||||
|
|
||||||
|
export default function github(): Provider {
|
||||||
return {
|
return {
|
||||||
name: 'github',
|
name: 'github',
|
||||||
domains: [
|
domains: [
|
||||||
@ -29,14 +31,17 @@ export default function github() {
|
|||||||
canSelfHost: true,
|
canSelfHost: true,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
getIsLightTheme: () =>
|
getIsLightTheme: () =>
|
||||||
document.querySelector('html').getAttribute('data-color-mode') === 'light',
|
document.querySelector('html')?.getAttribute('data-color-mode') ===
|
||||||
|
'light',
|
||||||
getIsDirectory: ({ icon }) =>
|
getIsDirectory: ({ icon }) =>
|
||||||
icon.getAttribute('aria-label') === 'Directory' ||
|
icon.getAttribute('aria-label') === 'Directory' ||
|
||||||
icon.classList.contains('octicon-file-directory-fill') ||
|
icon.classList.contains('octicon-file-directory-fill') ||
|
||||||
icon.classList.contains('octicon-file-directory-open-fill') ||
|
icon.classList.contains('octicon-file-directory-open-fill') ||
|
||||||
icon.classList.contains('icon-directory'),
|
icon.classList.contains('icon-directory'),
|
||||||
getIsSubmodule: ({ icon }) => icon.getAttribute('aria-label') === 'Submodule',
|
getIsSubmodule: ({ icon }) =>
|
||||||
getIsSymlink: ({ icon }) => icon.getAttribute('aria-label') === 'Symlink Directory',
|
icon.getAttribute('aria-label') === 'Submodule',
|
||||||
|
getIsSymlink: ({ icon }) =>
|
||||||
|
icon.getAttribute('aria-label') === 'Symlink Directory',
|
||||||
replaceIcon: (svgEl, newSVG) => {
|
replaceIcon: (svgEl, newSVG) => {
|
||||||
svgEl
|
svgEl
|
||||||
.getAttributeNames()
|
.getAttributeNames()
|
||||||
@ -44,7 +49,7 @@ export default function github() {
|
|||||||
(attr) =>
|
(attr) =>
|
||||||
attr !== 'src' &&
|
attr !== 'src' &&
|
||||||
!/^data-material-icons-extension/.test(attr) &&
|
!/^data-material-icons-extension/.test(attr) &&
|
||||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
const prevEl = svgEl.previousElementSibling;
|
const prevEl = svgEl.previousElementSibling;
|
@ -1,4 +1,6 @@
|
|||||||
export default function gitlab() {
|
import { Provider } from '../models';
|
||||||
|
|
||||||
|
export default function gitlab(): Provider {
|
||||||
return {
|
return {
|
||||||
name: 'gitlab',
|
name: 'gitlab',
|
||||||
domains: [
|
domains: [
|
||||||
@ -20,11 +22,14 @@ export default function gitlab() {
|
|||||||
},
|
},
|
||||||
canSelfHost: true,
|
canSelfHost: true,
|
||||||
isCustom: false,
|
isCustom: false,
|
||||||
getIsLightTheme: () => !document.querySelector('body').classList.contains('gl-dark'),
|
getIsLightTheme: () =>
|
||||||
getIsDirectory: ({ icon }) => icon.getAttribute('data-testid') === 'folder-icon',
|
!document.querySelector('body')?.classList.contains('gl-dark'),
|
||||||
|
getIsDirectory: ({ icon }) =>
|
||||||
|
icon.getAttribute('data-testid') === 'folder-icon',
|
||||||
getIsSubmodule: ({ row }) =>
|
getIsSubmodule: ({ row }) =>
|
||||||
row.querySelector('a')?.classList.contains('is-submodule') || false,
|
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) => {
|
replaceIcon: (svgEl, newSVG) => {
|
||||||
svgEl
|
svgEl
|
||||||
.getAttributeNames()
|
.getAttributeNames()
|
||||||
@ -32,13 +37,13 @@ export default function gitlab() {
|
|||||||
(attr) =>
|
(attr) =>
|
||||||
attr !== 'src' &&
|
attr !== 'src' &&
|
||||||
!/^data-material-icons-extension/.test(attr) &&
|
!/^data-material-icons-extension/.test(attr) &&
|
||||||
newSVG.setAttribute(attr, svgEl.getAttribute(attr))
|
newSVG.setAttribute(attr, svgEl.getAttribute(attr) ?? '')
|
||||||
);
|
);
|
||||||
|
|
||||||
newSVG.style.height = '16px';
|
newSVG.style.height = '16px';
|
||||||
newSVG.style.width = '16px';
|
newSVG.style.width = '16px';
|
||||||
|
|
||||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||||
},
|
},
|
||||||
onAdd: () => {},
|
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 { 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,
|
azure,
|
||||||
bitbucket,
|
bitbucket,
|
||||||
gitea,
|
gitea,
|
||||||
@ -17,7 +18,7 @@ export const providers = {
|
|||||||
sourceforge,
|
sourceforge,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const providerConfig = {};
|
export const providerConfig: Record<string, Provider> = {};
|
||||||
|
|
||||||
for (const provider of Object.values(providers)) {
|
for (const provider of Object.values(providers)) {
|
||||||
const cfg = provider();
|
const cfg = provider();
|
||||||
@ -25,17 +26,17 @@ for (const provider of Object.values(providers)) {
|
|||||||
providerConfig[cfg.name] = cfg;
|
providerConfig[cfg.name] = cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
function regExpEscape(str) {
|
function regExpEscape(value: string) {
|
||||||
return str.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, '\\$&');
|
return value.replace(/[-[\]{}()*+!<=:?./\\^$|#\s,]/g, '\\$&');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add custom git provider
|
* 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;
|
handler = typeof handler === 'string' ? providers[handler] : handler;
|
||||||
|
|
||||||
const provider = handler();
|
const provider = handler();
|
||||||
@ -64,12 +65,8 @@ export const getGitProviders = () =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all selectors and functions specific to the Git provider
|
* 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')) {
|
if (!domain.startsWith('http')) {
|
||||||
domain = new URL(`http://${domain}`).host;
|
domain = new URL(`http://${domain}`).host;
|
||||||
} else {
|
} else {
|
@ -1,4 +1,6 @@
|
|||||||
export default function sourceforge() {
|
import { Provider } from '../models';
|
||||||
|
|
||||||
|
export default function sourceforge(): Provider {
|
||||||
return {
|
return {
|
||||||
name: 'sourceforge',
|
name: 'sourceforge',
|
||||||
domains: [
|
domains: [
|
||||||
@ -42,11 +44,15 @@ export default function sourceforge() {
|
|||||||
newSVG.style.height = '14px';
|
newSVG.style.height = '14px';
|
||||||
newSVG.style.width = '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
|
// For the files list, use the anchor element instead of the icon because in some cases there is no icon
|
||||||
else {
|
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
|
// only replace/prepend the icon once
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -57,7 +63,7 @@ export default function sourceforge() {
|
|||||||
const svgEl = iconOrAnchor.querySelector('svg');
|
const svgEl = iconOrAnchor.querySelector('svg');
|
||||||
|
|
||||||
if (svgEl) {
|
if (svgEl) {
|
||||||
svgEl.parentNode.replaceChild(newSVG, svgEl);
|
svgEl.parentNode?.replaceChild(newSVG, svgEl);
|
||||||
} else {
|
} else {
|
||||||
iconOrAnchor.prepend(newSVG);
|
iconOrAnchor.prepend(newSVG);
|
||||||
}
|
}
|
@ -48,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="footer" class="centered">
|
<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"
|
><img src="settings-popup.github-logo.svg"
|
||||||
/></a>
|
/></a>
|
||||||
</div>
|
</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;
|
margin: 0;
|
||||||
color: #1a202c;
|
color: #1a202c;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
|
@ -18,9 +18,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="request">
|
<div id="request">
|
||||||
We need access to the website to show the icons. If you click the button bellow the
|
We need access to the website to show the icons. If you click the
|
||||||
browser will ask you for permision in another popup, this one will close. If you allow the
|
button bellow the browser will ask you for permision in another popup,
|
||||||
permission you will have to come back to finish the setup.
|
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">
|
<button type="button" id="request-access" class="btn">
|
||||||
<span>Allow access to website</span>
|
<span>Allow access to website</span>
|
||||||
@ -28,11 +29,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="not-supported">
|
<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>
|
||||||
|
|
||||||
<div id="default-disabled-note">
|
<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>
|
<a id="options-link">options page</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -62,7 +65,10 @@
|
|||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -86,7 +92,10 @@
|
|||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -105,7 +114,10 @@
|
|||||||
focusable="false"
|
focusable="false"
|
||||||
aria-hidden="true"
|
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>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -118,7 +130,7 @@
|
|||||||
|
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/Claudiohbsantos/github-material-icons-extension"
|
href="https://github.com/material-extensions/material-icons-browser-addon"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
><img src="settings-popup.github-logo.svg"
|
><img src="settings-popup.github-logo.svg"
|
||||||
/></a>
|
/></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