├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── README.md ├── commitlint.config.js ├── package.json ├── postcss.config.js ├── res ├── 1400x560.png ├── 440x280.png ├── 920x680.png ├── icon.png ├── promotional.psd ├── screenshot-en.png ├── screenshot-en.psd ├── screenshot-zh-cn.png └── screenshot-zh-cn.psd ├── src ├── _locales │ ├── en │ │ └── messages.json │ └── zh_CN │ │ └── messages.json ├── assets │ └── images │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-32.png │ │ ├── icon-48.png │ │ └── icon-64.png ├── components │ ├── icon-table.tsx │ └── popup.tsx ├── globals.css ├── manifest.dev.json ├── manifest.prod.json ├── popup │ ├── index.html │ └── index.tsx └── utils │ ├── compute-icon-area.ts │ ├── get-icons-from-page.ts │ ├── get-max-size.ts │ ├── get-page-info.ts │ ├── get-unique-icon-types.ts │ └── i18n.ts ├── tailwind.config.js ├── tsconfig.json ├── webpack.common.js ├── webpack.dev.js ├── webpack.prod.js └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | 4 | # Build 5 | dist 6 | lib 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true 3 | , parser: '@typescript-eslint/parser' 4 | , plugins: [ 5 | '@typescript-eslint' 6 | , 'react' 7 | , 'react-hooks' 8 | ] 9 | , extends: [ 10 | 'eslint:recommended' 11 | , 'plugin:@typescript-eslint/recommended' 12 | , 'plugin:react/recommended' 13 | , 'plugin:react/jsx-runtime' 14 | , 'plugin:react-hooks/recommended' 15 | ] 16 | , rules: { 17 | 'no-constant-condition': 'off' 18 | , '@typescript-eslint/no-extra-semi': 'off' 19 | } 20 | , settings: { 21 | react: { 22 | 'version': 'detect' 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [18.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install -g yarn 21 | - run: yarn install 22 | - run: yarn lint 23 | - run: yarn build 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Build 40 | dist 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 BlackGlory 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Favicon Detector 2 | ![favicon-detector-logo] 3 | 4 | A simple way to detect website icons based on [parse-favicon] library. 5 | 6 | [favicon-detector-logo]: src/assets/images/icon-128.png 7 | [parse-favicon]: https://github.com/BlackGlory/parse-favicon 8 | 9 | ## Supported browsers and platforms 10 | - [x] [Chrome] 11 | - [x] [Edge] 12 | - [x] [Firefox] (old version [#11]) 13 | 14 | [Chrome]: https://chrome.google.com/webstore/detail/jlfeffjhgmgblofcgpbgpkkhfniipejm 15 | [Firefox]: https://addons.mozilla.org/firefox/addon/favicon-detector/ 16 | [Edge]: https://microsoftedge.microsoft.com/addons/detail/favicon-detector/kmlcolmkcjbpagopgiojkflkejfnnjbm 17 | [#11]: https://github.com/BlackGlory/favicon-detector/issues/11 18 | 19 | ## Build 20 | ```sh 21 | yarn install 22 | yarn build 23 | ``` 24 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'] 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "name": "favicion-detector", 4 | "repository": "git@github.com:BlackGlory/favicon-detector.git", 5 | "author": "BlackGlory ", 6 | "license": "MIT", 7 | "scripts": { 8 | "lint": "eslint --ext .js,.jsx,.ts,.tsx --quiet src", 9 | "clean": "rimraf dist", 10 | "build": "webpack --config webpack.prod.js", 11 | "dev": "webpack --watch --config webpack.dev.js" 12 | }, 13 | "husky": { 14 | "hooks": { 15 | "pre-commit": "run-s lint clean build", 16 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 17 | } 18 | }, 19 | "devDependencies": { 20 | "@commitlint/cli": "^18.4.3", 21 | "@commitlint/config-conventional": "^18.4.3", 22 | "@types/chrome": "^0.0.254", 23 | "@types/object-hash": "^3.0.6", 24 | "@types/react": "^18.2.45", 25 | "@types/react-dom": "^18.2.18", 26 | "@types/styled-components": "^5.1.34", 27 | "@typescript-eslint/eslint-plugin": "^6.15.0", 28 | "@typescript-eslint/parser": "^6.15.0", 29 | "assert": "^2.1.0", 30 | "autoprefixer": "^10.4.16", 31 | "buffer": "^6.0.3", 32 | "copy-webpack-plugin": "^11.0.0", 33 | "css-loader": "^6.8.1", 34 | "eslint": "^8.56.0", 35 | "eslint-plugin-react": "^7.33.2", 36 | "eslint-plugin-react-hooks": "^4.6.0", 37 | "events": "^3.3.0", 38 | "husky": "^4.3.8", 39 | "npm-run-all": "^4.1.5", 40 | "path-browserify": "^1.0.1", 41 | "postcss": "^8.4.32", 42 | "postcss-loader": "^7.3.3", 43 | "process": "^0.11.10", 44 | "rimraf": "^5.0.5", 45 | "stream-browserify": "^3.0.0", 46 | "style-loader": "^3.3.3", 47 | "tailwindcss": "^3.4.0", 48 | "terser-webpack-plugin": "^5.3.9", 49 | "ts-loader": "^9.5.1", 50 | "tsconfig-paths-webpack-plugin": "^4.1.0", 51 | "typescript": "5.3.3", 52 | "util": "^0.12.5", 53 | "webpack": "^5.89.0", 54 | "webpack-cli": "^5.1.4", 55 | "webpack-merge": "^5.10.0" 56 | }, 57 | "dependencies": { 58 | "@badrap/bar-of-progress": "^0.2.2", 59 | "@blackglory/prelude": "^0.3.4", 60 | "antd": "^5.12.4", 61 | "extra-blob": "^0.1.2", 62 | "extra-react-hooks": "^0.6.6", 63 | "extra-utils": "^5.5.2", 64 | "extra-webextension": "^0.4.0", 65 | "immer": "^10.0.3", 66 | "iterable-operator": "^4.0.6", 67 | "object-hash": "^3.0.0", 68 | "parse-favicon": "^7.0.1", 69 | "react": "^18.2.0", 70 | "react-dom": "^18.2.0", 71 | "react-is": "^18.2.0", 72 | "rxjs": "^7.8.1", 73 | "tailwind-merge": "^2.1.0", 74 | "use-immer": "^0.9.0" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {} 4 | , autoprefixer: {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /res/1400x560.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/1400x560.png -------------------------------------------------------------------------------- /res/440x280.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/440x280.png -------------------------------------------------------------------------------- /res/920x680.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/920x680.png -------------------------------------------------------------------------------- /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/icon.png -------------------------------------------------------------------------------- /res/promotional.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/promotional.psd -------------------------------------------------------------------------------- /res/screenshot-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/screenshot-en.png -------------------------------------------------------------------------------- /res/screenshot-en.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/screenshot-en.psd -------------------------------------------------------------------------------- /res/screenshot-zh-cn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/screenshot-zh-cn.png -------------------------------------------------------------------------------- /res/screenshot-zh-cn.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/res/screenshot-zh-cn.psd -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Favicon Detector" 4 | } 5 | , "extensionDescription": { 6 | "message": "A simple way to detect website icons." 7 | } 8 | , "extensionBrowserActionTitle": { 9 | "message": "Analyze the icon of the current page" 10 | } 11 | , "titleReference": { 12 | "message": "Reference" 13 | } 14 | , "titleType": { 15 | "message": "Type" 16 | } 17 | , "titleSize": { 18 | "message": "Size" 19 | } 20 | , "titleIcon": { 21 | "message": "Icon" 22 | } 23 | , "messageImageURLCopied": { 24 | "message": "Image URL copied" 25 | } 26 | , "messageImageCopied": { 27 | "message": "Image copied" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "网站图标探测器" 4 | } 5 | , "extensionDescription": { 6 | "message": "探测网站图标的简单方式。" 7 | } 8 | , "extensionBrowserActionTitle": { 9 | "message": "分析当前页面的图标" 10 | } 11 | , "titleReference": { 12 | "message": "参考" 13 | } 14 | , "titleType": { 15 | "message": "类型" 16 | } 17 | , "titleSize": { 18 | "message": "尺寸" 19 | } 20 | , "titleIcon": { 21 | "message": "图标" 22 | } 23 | , "messageImageURLCopied": { 24 | "message": "图片链接已复制" 25 | } 26 | , "messageImageCopied": { 27 | "message": "图片已复制" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/assets/images/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/src/assets/images/icon-128.png -------------------------------------------------------------------------------- /src/assets/images/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/src/assets/images/icon-16.png -------------------------------------------------------------------------------- /src/assets/images/icon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/src/assets/images/icon-32.png -------------------------------------------------------------------------------- /src/assets/images/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/src/assets/images/icon-48.png -------------------------------------------------------------------------------- /src/assets/images/icon-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlackGlory/favicon-detector/6d5e5ac56bde53a08c014013fd5b32006ae97047/src/assets/images/icon-64.png -------------------------------------------------------------------------------- /src/components/icon-table.tsx: -------------------------------------------------------------------------------- 1 | import Table, { ColumnsType } from 'antd/lib/table' 2 | import Message from 'antd/lib/message' 3 | import { isArray, isNull, isString } from '@blackglory/prelude' 4 | import hash from 'object-hash' 5 | import { IIcon } from 'parse-favicon' 6 | import { i18n } from '@utils/i18n' 7 | import { getMaxSize } from '@utils/get-max-size' 8 | import { computeIconArea } from '@utils/compute-icon-area' 9 | import { getUniqueIconTypes } from '@utils/get-unique-icon-types' 10 | import { twMerge } from 'tailwind-merge' 11 | import { textToBlob } from 'extra-blob' 12 | 13 | interface ISize { 14 | width: number 15 | height: number 16 | } 17 | 18 | interface IIconTableProps { 19 | icons: IIcon[] 20 | } 21 | 22 | export function IconTable({ icons }: IIconTableProps) { 23 | const columns: ColumnsType = [ 24 | { 25 | title: i18n('titleIcon') 26 | , dataIndex: 'url' 27 | , key: 'icon' 28 | , render(_, icon: IIcon) { 29 | return 30 | } 31 | } 32 | , { 33 | title: i18n('titleSize') 34 | , dataIndex: 'size' 35 | , key: 'size' 36 | , defaultSortOrder: 'ascend' 37 | , render(_, icon) { 38 | return iconSizeToString(icon) 39 | } 40 | , sorter(a, b) { 41 | return computeIconArea(a) - computeIconArea(b) 42 | } 43 | } 44 | , { 45 | title: i18n('titleType') 46 | , dataIndex: 'type' 47 | , key: 'type' 48 | , render(_, icon) { 49 | return iconTypeToString(icon) 50 | } 51 | , filters: getUniqueIconTypes(icons).map(toFilter) 52 | , onFilter(value, record) { 53 | return record.type === value 54 | } 55 | } 56 | , { 57 | title: i18n('titleReference') 58 | , dataIndex: 'reference' 59 | , key: 'reference' 60 | } 61 | ] 62 | 63 | return ( 64 | hash(record)} 67 | sticky={true} 68 | tableLayout='auto' 69 | pagination={false} 70 | dataSource={icons} 71 | columns={columns} 72 | /> 73 | ) 74 | } 75 | 76 | function toFilter(x: string): { 77 | text: string 78 | value: string 79 | } { 80 | return { 81 | text: x 82 | , value: x 83 | } 84 | } 85 | 86 | function iconTypeToString(icon: IIcon): string { 87 | const type = icon.type 88 | return type ?? 'unknown' 89 | } 90 | 91 | function iconSizeToString(icon: IIcon): string { 92 | if (isNull(icon.size)) { 93 | return 'unknown' 94 | } else if (isString(icon.size)) { 95 | return icon.size 96 | } else if (isArray(icon.size)) { 97 | return icon.size.map(sizeToString).join(' ') 98 | } else { 99 | return sizeToString(icon.size) 100 | } 101 | 102 | function sizeToString(size: { width: number; height: number }): string { 103 | return `${size.width}x${size.height}` 104 | } 105 | } 106 | 107 | function Icon({ icon }: { icon: IIcon }) { 108 | if (icon.size) { 109 | if (isArray(icon.size)) { 110 | const size = getMaxSize(icon.size) 111 | return 116 | } else if (icon.size !== 'any') { 117 | return 122 | } 123 | } 124 | 125 | return 126 | } 127 | 128 | function CopyableImage(props: React.ComponentPropsWithoutRef<'img'>) { 129 | return { 137 | if (props.src) { 138 | try { 139 | await writeImageClipboard(props.src) 140 | } catch { 141 | await writeImageURLToClipboard(props.src) 142 | } 143 | } 144 | }} 145 | /> 146 | 147 | async function writeImageClipboard(imageUrl: string): Promise { 148 | const res = await fetch(imageUrl, { cache: 'force-cache' }) 149 | const blob = await res.blob() 150 | 151 | const clipboardItem = new ClipboardItem({ 152 | 'text/plain': textToBlob(imageUrl) 153 | , 'text/html': textToBlob(createImgHTML(imageUrl), 'text/html') 154 | , [blob.type]: blob 155 | }) 156 | await navigator.clipboard.write([clipboardItem]) 157 | 158 | Message.success(i18n('messageImageCopied')) 159 | } 160 | 161 | async function writeImageURLToClipboard(imageUrl: string): Promise { 162 | const clipboardItem = new ClipboardItem({ 163 | 'text/plain': textToBlob(imageUrl) 164 | , 'text/html': textToBlob(createImgHTML(imageUrl), 'text/html') 165 | }) 166 | 167 | await navigator.clipboard.write([clipboardItem]) 168 | 169 | Message.success(i18n('messageImageURLCopied')) 170 | } 171 | 172 | function createImgHTML(imageUrl: string): string { 173 | return `` 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/components/popup.tsx: -------------------------------------------------------------------------------- 1 | import ProgressBar from '@badrap/bar-of-progress' 2 | import Message from 'antd/lib/message' 3 | import { IconTable } from '@components/icon-table' 4 | import { useImmer } from 'use-immer' 5 | import hash from 'object-hash' 6 | import { IIcon } from 'parse-favicon' 7 | import { getIconsFromPage } from '@utils/get-icons-from-page' 8 | import { useMountAsync } from 'extra-react-hooks' 9 | 10 | export function Popup() { 11 | const [iconByHash, updateIconByHash] = useImmer>({}) 12 | const icons: IIcon[] = Object.values(iconByHash) 13 | 14 | useMountAsync(async () => { 15 | const progress = new ProgressBar() 16 | progress.start() 17 | 18 | try { 19 | const observable = await getIconsFromPage() 20 | observable.subscribe({ 21 | next(icon) { 22 | updateIconByHash(icons => { 23 | icons[hash(icon)] = icon 24 | }) 25 | } 26 | , error(err) { 27 | progress.finish() 28 | console.error(err) 29 | Message.error(err.message, 0) 30 | } 31 | , complete() { 32 | progress.finish() 33 | } 34 | }) 35 | } catch (e) { 36 | progress.finish() 37 | 38 | throw e 39 | } 40 | }) 41 | 42 | return ( 43 |
44 | 45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | /* 通过强制固定窗口尺寸, 以修复在弹出Antd.Message时Chrome无法收缩窗口的bug. */ 7 | html, body { 8 | min-width: 800px; 9 | min-height: 600px; 10 | width: 800px; 11 | height: 600px; 12 | overflow: hidden; 13 | } 14 | } 15 | 16 | @layer utilities { 17 | .bg-transparent-fake { 18 | background-image: 19 | linear-gradient(45deg, #b0b0b0 25%, transparent 25%), 20 | linear-gradient(-45deg, #b0b0b0 25%, transparent 25%), 21 | linear-gradient(45deg, transparent 75%, #b0b0b0 75%), 22 | linear-gradient(-45deg, transparent 75%, #b0b0b0 75%); 23 | background-position: 0 0, 0 10px, 10px -10px, -10px 0px; 24 | background-size: 20px 20px; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/manifest.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extensionName__" 3 | , "author": "BlackGlory" 4 | , "version": "4.1.2" 5 | , "manifest_version": 3 6 | , "default_locale": "en" 7 | , "description": "__MSG_extensionDescription__" 8 | , "icons": { 9 | "16": "assets/images/icon-16.png" 10 | , "32": "assets/images/icon-32.png" 11 | , "48": "assets/images/icon-48.png" 12 | , "64": "assets/images/icon-64.png" 13 | , "128": "assets/images/icon-128.png" 14 | } 15 | , "action": { 16 | "default_title": "__MSG_extensionBrowserActionTitle__" 17 | , "default_popup": "popup.html" 18 | , "default_icon": { 19 | "16": "assets/images/icon-16.png" 20 | , "32": "assets/images/icon-32.png" 21 | , "48": "assets/images/icon-48.png" 22 | , "64": "assets/images/icon-64.png" 23 | , "128": "assets/images/icon-128.png" 24 | } 25 | } 26 | , "permissions": [ 27 | "activeTab" 28 | , "scripting" 29 | ] 30 | , "host_permissions": [ 31 | "" 32 | ] 33 | , "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkRQGhQtxVngxnfz5BysYrl0QF7clqlrdOhQw0BnnuG8k5Ay6Fs3kbpWuXAee7pu2kEQ/+V91IUA6JrT3MswMF3oDWT5gap2+oRq6rZBwz1PWuFpxCimu6XJrVZZmgS+jS4xmmhFVS24CVw5QiEmE2iRLvRnCJLvsvqAuNo5Pv+WuQNciT05NIwgxfFaO5CGq8fvQ0imFtpfc8frCcTBWvbLVRMWj+N+4UQqGJv/qQWvLJPKERl1CgciFsuK1Uh7HX6fH2UsXt4H4gNVr5vmAIQqxEX9Q/qlcJ/4JYgydoGsHDNDby++c4XjWiPr1y6tUCrHSvNM3qp9EauTpTh6oLQIDAQAB" 34 | } 35 | -------------------------------------------------------------------------------- /src/manifest.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "__MSG_extensionName__" 3 | , "author": "BlackGlory" 4 | , "version": "4.1.2" 5 | , "manifest_version": 3 6 | , "default_locale": "en" 7 | , "description": "__MSG_extensionDescription__" 8 | , "icons": { 9 | "16": "assets/images/icon-16.png" 10 | , "32": "assets/images/icon-32.png" 11 | , "48": "assets/images/icon-48.png" 12 | , "64": "assets/images/icon-64.png" 13 | , "128": "assets/images/icon-128.png" 14 | } 15 | , "action": { 16 | "default_title": "__MSG_extensionBrowserActionTitle__" 17 | , "default_popup": "popup.html" 18 | , "default_icon": { 19 | "16": "assets/images/icon-16.png" 20 | , "32": "assets/images/icon-32.png" 21 | , "48": "assets/images/icon-48.png" 22 | , "64": "assets/images/icon-64.png" 23 | , "128": "assets/images/icon-128.png" 24 | } 25 | } 26 | , "permissions": [ 27 | "activeTab" 28 | , "scripting" 29 | ] 30 | , "host_permissions": [ 31 | "" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/popup/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Favicon Detector 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/popup/index.tsx: -------------------------------------------------------------------------------- 1 | import '@src/globals.css' 2 | import 'antd/dist/reset.css' 3 | import { createRoot } from 'react-dom/client' 4 | import { Popup } from '@components/popup' 5 | import { assert } from '@blackglory/prelude' 6 | 7 | const main = document.querySelector('main') 8 | assert(main, 'The main element not found') 9 | 10 | const root = createRoot(main) 11 | root.render() 12 | -------------------------------------------------------------------------------- /src/utils/compute-icon-area.ts: -------------------------------------------------------------------------------- 1 | import { IIcon } from 'parse-favicon' 2 | import { isArray, isNull, isString } from '@blackglory/prelude' 3 | 4 | export function computeIconArea(icon: IIcon): number { 5 | return computeSizeArea(icon.size) 6 | } 7 | 8 | function computeSizeArea(size: IIcon['size']): number { 9 | if (isNull(size)) { 10 | return 0 11 | } else if (isString(size)) { 12 | return Infinity 13 | } else if (isArray(size)) { 14 | return size.map(computeSizeArea).reduce((ret, cur) => Math.max(ret, cur)) 15 | } else { 16 | return size.width * size.height 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/get-icons-from-page.ts: -------------------------------------------------------------------------------- 1 | import { parseFavicon, IIcon } from 'parse-favicon' 2 | import { getPageInfo } from '@utils/get-page-info' 3 | import { Observable } from 'rxjs' 4 | import { map } from 'rxjs/operators' 5 | import { assert, Awaitable } from '@blackglory/prelude' 6 | import { produce } from 'immer' 7 | import { getActiveTab } from 'extra-webextension' 8 | 9 | export async function getIconsFromPage(): Promise> { 10 | const activeTab = await getActiveTab() 11 | assert(activeTab.id, 'The activeTab.id is undefined') 12 | 13 | const page = await getPageInfo(activeTab.id) 14 | 15 | return parseFavicon(page.url, textFetcher, bufferFetcher) 16 | .pipe( 17 | map(icon => produce(icon, icon => { 18 | icon.url = new URL(icon.url, page.url).href 19 | })) 20 | ) 21 | 22 | function textFetcher(url: string): Awaitable { 23 | if (url === page.url) { 24 | return page.html 25 | } else { 26 | return fetch(url) 27 | .then(res => res.text()) 28 | } 29 | } 30 | 31 | function bufferFetcher(url: string): Awaitable { 32 | return fetch(url) 33 | .then(res => res.arrayBuffer()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/get-max-size.ts: -------------------------------------------------------------------------------- 1 | export function getMaxSize(sizes: Array<{ width: number, height: number }>): { 2 | width: number 3 | height: number 4 | } { 5 | return sizes.reduce((ret, cur) => { 6 | if (computeSizeArea(cur) > computeSizeArea(ret)) { 7 | return cur 8 | } else { 9 | return ret 10 | } 11 | }) 12 | } 13 | 14 | function computeSizeArea(size: { width: number, height: number }): number { 15 | return size.width * size.height 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/get-page-info.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | export async function getPageInfo(tabId: number): Promise<{ 4 | url: string 5 | , html: string 6 | }> { 7 | const results = await chrome.scripting.executeScript({ 8 | target: { tabId } 9 | , injectImmediately: false 10 | , func: () => { 11 | return { 12 | url: document.URL 13 | , html: document.documentElement.outerHTML 14 | } 15 | } 16 | }) 17 | 18 | const result = results[0].result 19 | assert(result, 'The result is undefined') 20 | 21 | return result 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/get-unique-icon-types.ts: -------------------------------------------------------------------------------- 1 | import { IIcon } from 'parse-favicon' 2 | import { pipe } from 'extra-utils' 3 | import { map, filter, uniq, toArray } from 'iterable-operator' 4 | 5 | export function getUniqueIconTypes(icons: IIcon[]): string[] { 6 | return pipe( 7 | icons 8 | , icons => map(icons, x => x.type) 9 | , types => filter(types, x => !!x) 10 | , uniq 11 | , toArray 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | export function i18n(message: string): string { 2 | return chrome.i18n.getMessage(message) 3 | } 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/**/*.{html,js,ts,jsx,tsx}' 5 | ] 6 | , theme: { 7 | extend: {} 8 | } 9 | , plugins: [] 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018" 4 | , "module": "ES2015" 5 | , "moduleResolution": "Node" 6 | , "jsx": "react-jsx" 7 | , "removeComments": true 8 | , "strict": true 9 | , "noUnusedParameters": false 10 | , "esModuleInterop": true 11 | , "baseUrl": "." 12 | , "paths": { 13 | "@src/*": ["src/*"], 14 | "@utils/*": ["src/utils/*"], 15 | "@components/*": ["src/components/*"] 16 | } 17 | } 18 | , "include": [ 19 | "src" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CopyPlugin = require('copy-webpack-plugin') 3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin') 4 | const { ProvidePlugin, NormalModuleReplacementPlugin } = require('webpack') 5 | 6 | module.exports = { 7 | target: 'web' 8 | , entry: { 9 | 'popup': './src/popup/index.tsx' 10 | } 11 | , output: { 12 | path: path.join(__dirname, 'dist') 13 | , chunkFilename: 'chunk.[name].js' // 防止生成以`_`开头的文件名, 因为Chrome保留了这些文件名. 14 | , filename: '[name].js' 15 | } 16 | , resolve: { 17 | extensions: ['.ts', '.tsx', '.js', '.json'] 18 | , plugins: [new TsconfigPathsPlugin()] 19 | , fallback: { 20 | 'util': require.resolve('util') 21 | , 'path': require.resolve('path-browserify') 22 | , 'assert': require.resolve('assert') 23 | , 'buffer': require.resolve('buffer') 24 | , 'stream': require.resolve('stream-browserify') 25 | , 'process': require.resolve('process/browser') 26 | , 'events': require.resolve('events') 27 | , 'fs': false 28 | } 29 | } 30 | , module: { 31 | rules: [ 32 | { 33 | test: /\.tsx?$/ 34 | , exclude: /node_module/ 35 | , use: 'ts-loader' 36 | } 37 | , { 38 | test: /\.css$/i, 39 | use: ['style-loader', 'css-loader', 'postcss-loader'], 40 | } 41 | ] 42 | } 43 | , plugins: [ 44 | new NormalModuleReplacementPlugin(/^node:/, (resource) => { 45 | const mod = resource.request.replace(/^node:/, '') 46 | 47 | switch (mod) { 48 | case 'buffer': { 49 | resource.request = 'buffer' 50 | break 51 | } 52 | case 'stream': { 53 | resource.request = 'stream' 54 | break 55 | } 56 | default: throw new Error(`Not found ${mod}`) 57 | } 58 | }) 59 | , new ProvidePlugin({ 60 | process: 'process' 61 | , Buffer: ['buffer', 'Buffer'] 62 | }) 63 | , new CopyPlugin({ 64 | patterns: [ 65 | { 66 | from: './src' 67 | , globOptions: { 68 | ignore: ['**/*.ts', '**/*.tsx', '**/*.html', '**/manifest.*.json'] 69 | } 70 | } 71 | , { from: './src/popup/index.html', to: 'popup.html' } 72 | ] 73 | }) 74 | ] 75 | } 76 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common') 3 | const CopyPlugin = require('copy-webpack-plugin') 4 | 5 | module.exports = merge(common, { 6 | mode: 'development' 7 | , devtool: 'inline-source-map' 8 | , plugins: [ 9 | new CopyPlugin({ 10 | patterns: [ 11 | { from: './src/manifest.dev.json', to: 'manifest.json' } 12 | ] 13 | }) 14 | ] 15 | }) 16 | -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge') 2 | const common = require('./webpack.common') 3 | const TerserPlugin = require('terser-webpack-plugin') 4 | const CopyPlugin = require('copy-webpack-plugin') 5 | 6 | module.exports = merge(common, { 7 | mode: 'production' 8 | , devtool: 'source-map' 9 | , optimization: { 10 | minimize: true 11 | , minimizer: [ 12 | new TerserPlugin() 13 | ] 14 | } 15 | , plugins: [ 16 | new CopyPlugin({ 17 | patterns: [ 18 | { from: './src/manifest.prod.json', to: 'manifest.json' } 19 | ] 20 | }) 21 | ] 22 | }) 23 | --------------------------------------------------------------------------------