├── icon ├── 128.png ├── 16.png ├── 256.png ├── 32.png ├── 48.png ├── 64.png └── 96.png ├── .editorconfig ├── bg ├── bg.js ├── bg-install.js └── bg-xhr.js ├── _locales ├── en │ └── messages.json └── zh_CN │ └── messages.json ├── manifest.json ├── LICENSE ├── README.md ├── content ├── show-info.css └── show-info.js └── .eslintrc /icon/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/128.png -------------------------------------------------------------------------------- /icon/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/16.png -------------------------------------------------------------------------------- /icon/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/256.png -------------------------------------------------------------------------------- /icon/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/32.png -------------------------------------------------------------------------------- /icon/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/48.png -------------------------------------------------------------------------------- /icon/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/64.png -------------------------------------------------------------------------------- /icon/96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tophf/view-image-video-info/HEAD/icon/96.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /bg/bg.js: -------------------------------------------------------------------------------- 1 | import {fetchInfo} from './bg-xhr.js'; 2 | import './bg-install.js'; 3 | 4 | chrome.contextMenus.onClicked.addListener(async ({srcUrl, linkUrl, frameId}, tab) => { 5 | const CONTENT = '/content/show-info'; 6 | const MSG = {src: srcUrl, link: linkUrl}; 7 | const tabId = tab.id; 8 | const nop = () => {}; 9 | const exec = () => chrome.scripting.executeScript({ 10 | target: {tabId, frameIds: [frameId]}, 11 | files: [CONTENT + '.js'], 12 | // injectImmediately: true, // TODO: Chrome 102 13 | }).catch(nop); 14 | const getCss = () => fetch(CONTENT + '.css').then(r => r.text()); 15 | const send = msg => chrome.tabs.sendMessage(tabId, msg ?? MSG, {frameId}).catch(nop); 16 | const {id, src} = 17 | await send() || 18 | await Promise.all([getCss(), exec()]) 19 | .then(([css]) => send({...MSG, css})) || 20 | {}; 21 | if (src) send({id, info: await fetchInfo(src)}); 22 | }); 23 | -------------------------------------------------------------------------------- /_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "alt": { 3 | "message": "Alt" 4 | }, 5 | "author": { 6 | "message": "tophf + Dingbao.ai[aka. ehaagwlke]" 7 | }, 8 | "bytes": { 9 | "message": "bytes" 10 | }, 11 | "contextMenu": { 12 | "message": "View image/video info" 13 | }, 14 | "description": { 15 | "message": "Context menu for viewing image/video info (URL, dimensions, file size and type)" 16 | }, 17 | "dimensions": { 18 | "message": "Dimensions" 19 | }, 20 | "extName": { 21 | "message": "View image/video info" 22 | }, 23 | "fileSize": { 24 | "message": "File size" 25 | }, 26 | "fileType": { 27 | "message": "File type" 28 | }, 29 | "location": { 30 | "message": "Location" 31 | }, 32 | "scaledTo": { 33 | "message": "scaled to" 34 | }, 35 | "title": { 36 | "message": "Title" 37 | }, 38 | "typeImage": { 39 | "message": "image" 40 | }, 41 | "typeVideo": { 42 | "message": "video" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "minimum_chrome_version": "96", 4 | "name": "__MSG_extName__", 5 | "version": "1.0.1", 6 | "author": "__MSG_author__", 7 | "description": "__MSG_description__", 8 | "icons": { 9 | "16": "icon/16.png", 10 | "32": "icon/32.png", 11 | "48": "icon/48.png", 12 | "64": "icon/64.png", 13 | "96": "icon/96.png", 14 | "128": "icon/128.png", 15 | "256": "icon/256.png" 16 | }, 17 | "permissions": [ 18 | "activeTab", 19 | "contextMenus", 20 | "declarativeNetRequestWithHostAccess", 21 | "scripting" 22 | ], 23 | "host_permissions": [ 24 | "" 25 | ], 26 | "default_locale": "en", 27 | "key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDFCsceeED+YYEgjs62IOuxXEH0H/1cCsqiWp34spoHcSbkWC/8dP1oGlJci8Wpa6+bQkzpCVe14bVHpca8JZ7r8Y0ESs/NptuGKq57xsWgOJ/gnoxV1vWmO1pUTOiLSvxXYunFczMSsqNB3ZXdY3H0gmZzAXOnfDrLJNqWmZIVSwIDAQAB", 28 | "background": { 29 | "service_worker": "bg/bg.js", 30 | "type": "module" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 tophf 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 | -------------------------------------------------------------------------------- /_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extName":{ 3 | "message":"View Image Info (properties)" 4 | }, 5 | "author":{ 6 | "message":"艾丁宝[aka. ehaagwlke]" 7 | }, 8 | "description":{ 9 | "message":"通过右键菜单获取图片的尺寸,URL,文件大小,文件类型等信息" 10 | }, 11 | "contextMenu":{ 12 | "message":"查看图片信息" 13 | }, 14 | "ilam":{ 15 | "message":"图片下载取消" 16 | }, 17 | "errorLoading":{ 18 | "message":"图片下载错误" 19 | }, 20 | "location":{ 21 | "message":"图片位置" 22 | }, 23 | "dimensions":{ 24 | "message":"图片尺寸" 25 | }, 26 | "fileType":{ 27 | "message":"图片类型" 28 | }, 29 | "fileSize":{ 30 | "message":"文件大小" 31 | }, 32 | "preview":{ 33 | "message":"预览图片" 34 | }, 35 | "eifz":{ 36 | "message":"无法获取图片大小" 37 | }, 38 | "eift":{ 39 | "message":"无法获取图片类型" 40 | }, 41 | "eid":{ 42 | "message":"无法获取图片尺寸" 43 | }, 44 | "osize":{ 45 | "message":"原始尺寸" 46 | }, 47 | "ploc":{ 48 | "message":"页面地址" 49 | }, 50 | "alt":{ 51 | "message":"Alt" 52 | }, 53 | "Title":{ 54 | "message":"Title" 55 | }, 56 | "ealt":{ 57 | "message":"--" 58 | }, 59 | "eTit":{ 60 | "message":"--" 61 | }, 62 | "scaledTo":{ 63 | "message":"显示尺寸:" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /bg/bg-install.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onInstalled.addListener(() => { 2 | const opts = { 3 | type: 'normal', 4 | title: chrome.i18n.getMessage('contextMenu'), 5 | }; 6 | 7 | chrome.contextMenus.create({ 8 | ...opts, 9 | id: 'info', 10 | contexts: ['image', 'video'], 11 | documentUrlPatterns: ['*://*/*', 'file://*/*'], 12 | }); 13 | 14 | for (const [id, {pages, links}] of Object.entries({ 15 | 'imgur.com': { 16 | pages: ['/', '/t/*'], 17 | links: ['/gallery/*', '/t/*'], 18 | }, 19 | '*.facebook.com': { 20 | pages: ['/*'], 21 | links: ['/*/photos/*?type=*'], 22 | }, 23 | '500px.com': { 24 | pages: ['/*'], 25 | links: ['/photo/*'], 26 | }, 27 | 'www.instagram.com': { 28 | pages: ['/*'], 29 | links: ['/p/*'], 30 | }, 31 | })) { 32 | chrome.contextMenus.create({ 33 | ...opts, 34 | id: 'link:' + id, 35 | contexts: ['link'], 36 | documentUrlPatterns: pages.map(expandUrl, id), 37 | targetUrlPatterns: links.map(expandUrl, id), 38 | }); 39 | } 40 | 41 | function expandUrl(s) { 42 | return s.includes('://') ? s : `*://${this}${s}`; 43 | } 44 | }); 45 | -------------------------------------------------------------------------------- /bg/bg-xhr.js: -------------------------------------------------------------------------------- 1 | let ruleId = 0; 2 | 3 | export async function fetchInfo(url) { 4 | if (!/^https?:\/\//.test(url)) 5 | return; 6 | const NOP = () => {}; 7 | const RULE = { 8 | id: ++ruleId, 9 | condition: { 10 | domains: [chrome.runtime.id], // TODO: initiatorDomains in Chrome 102 11 | resourceTypes: ['xmlhttprequest'], 12 | urlFilter: url, 13 | }, 14 | action: { 15 | type: 'modifyHeaders', 16 | requestHeaders: [{ 17 | header: 'Referer', 18 | operation: 'set', 19 | value: new URL(url).origin + '/', 20 | }], 21 | }, 22 | }; 23 | await chrome.declarativeNetRequest.updateDynamicRules({ 24 | removeRuleIds: [RULE.id], 25 | addRules: [RULE], 26 | }); 27 | const ctl = new AbortController(); 28 | const timer = setTimeout(() => ctl.abort(), 20e3); 29 | const r = await fetch(url, {method: 'HEAD', signal: ctl.signal}).catch(NOP); 30 | const info = {}; 31 | if (!r || r.status >= 300) { 32 | info.error = true; 33 | } else { 34 | info.size = r.headers.get('Content-Length') | 0; 35 | info.type = r.headers.get('Content-Type'); 36 | } 37 | clearTimeout(timer); 38 | await chrome.declarativeNetRequest.updateDynamicRules({ 39 | removeRuleIds: [RULE.id], 40 | }); 41 | return info; 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### A fork of [View-Image-Info-Chrome](https://github.com/ehaagwlke/View-Image-Info-Chrome) extension 2 | 3 | logo 4 | 5 | Major changes: 6 | 7 | * google analytics is removed 8 | * video info is also shown 9 | * info is shown inside the page instead of a new window 10 | * background script runs only on demand when the extension's menu item is selected in the menu 11 | * content script runs only on demand when showing the info, and tries to find the clicked element retroactively 12 | 13 | Minor changes: 14 | 15 | * added an automatic dark theme via `prefers-color-scheme` 16 | * rewritten in modern JavaScript syntax 17 | * network request for the info is now a pure HEAD query 18 | * almost all visible text was slightly reworded 19 | * the UI was slightly restyled 20 | * the icon was redrawn using a shutter image by [Freepik](https://www.flaticon.com/authors/freepik) as a base 21 | 22 | ![ui](https://i.imgur.com/tWZGFGE.png) 23 | 24 | ### Permissions: 25 | 26 | * `contextMenus` - to add the context menu, duh 27 | * `` - to get the file size and type of the image 28 | 29 | ### How to limit the site permissions 30 | 31 | Chrome allows you to easily limit the extension so it can access only a few sites: 32 | 33 | 1. right-click the extension icon in the toolbar (or browser menu) and click "Manage" - it'll open `chrome://extensions` details page for this extension 34 | 2. click "On specific sites" 35 | 3. enter the URL you want to allow 36 | 4. to add more sites click "Add a new page" 37 | 38 | ![limit UI](https://i.imgur.com/F2nqVdL.png) 39 | -------------------------------------------------------------------------------- /content/show-info.css: -------------------------------------------------------------------------------- 1 | :host { 2 | all: initial !important; 3 | opacity: 0 !important; 4 | transition: opacity .15s cubic-bezier(.88, .02, .92, .66) !important; 5 | box-sizing: border-box !important; 6 | position: absolute !important; 7 | box-shadow: 3px 4px 20px rgba(0, 0, 0, 0.5) !important; 8 | z-index: 2147483647 !important; 9 | } 10 | main { 11 | background-color: papayawhip; 12 | color: #000; 13 | font: normal 14px sans-serif; 14 | white-space: nowrap; 15 | } 16 | table { 17 | border-spacing: 0; 18 | color: inherit; 19 | } 20 | table, tr, td { 21 | padding: 0; 22 | margin: 0; 23 | } 24 | tr:nth-child(even) { 25 | background-color: #8883; 26 | } 27 | td { 28 | line-height: 24px; 29 | padding-left: 4px; 30 | padding-right: 4px; 31 | height: 24px; 32 | } 33 | td:first-child { 34 | padding-left: 1em; 35 | } 36 | td:last-child { 37 | padding-right: 1em; 38 | } 39 | tr:first-child td { 40 | padding-top: .5em; 41 | } 42 | tr:last-child td { 43 | padding-bottom: .5em; 44 | } 45 | a { 46 | text-decoration: none; 47 | } 48 | a:hover { 49 | text-decoration: underline; 50 | } 51 | .gray { 52 | color: gray; 53 | } 54 | #url { 55 | max-width: 100px; 56 | display: block; 57 | overflow: hidden; 58 | text-overflow: ellipsis; 59 | white-space: nowrap; 60 | } 61 | #alt, #title { 62 | max-width: 50vw; 63 | white-space: normal; 64 | line-height: 1.2; 65 | } 66 | @media (min-width: 500px) { 67 | #alt, #title { 68 | max-width: 20em; 69 | } 70 | } 71 | #close { 72 | cursor: pointer; 73 | padding: .5ex 1ex; 74 | font: normal 15px/1.0 sans-serif; 75 | position: absolute; 76 | top: 0; 77 | right: 0; 78 | } 79 | #close:active { 80 | background-color: #f003; 81 | } 82 | #close:hover { 83 | background-color: #f803; 84 | } 85 | @media (prefers-color-scheme: dark) { 86 | main { 87 | background-color: #333; 88 | color: #aaa; 89 | } 90 | a { 91 | color: skyblue; 92 | } 93 | b { 94 | color: #bbb; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | # https://github.com/eslint/eslint/blob/master/docs/rules/README.md 2 | 3 | parser: babel-eslint 4 | 5 | parserOptions: 6 | ecmaVersion: 2021 7 | sourceType: script 8 | 9 | env: 10 | browser: true 11 | es6: true 12 | webextensions: true 13 | 14 | rules: 15 | accessor-pairs: [2] 16 | array-bracket-spacing: [2, never] 17 | array-callback-return: [0] 18 | arrow-body-style: [2, as-needed] 19 | arrow-parens: [2, as-needed] 20 | arrow-spacing: [2, {before: true, after: true}] 21 | block-scoped-var: [2] 22 | brace-style: [2] 23 | camelcase: [2, {properties: never}] 24 | class-methods-use-this: [2] 25 | comma-dangle: [2, {arrays: always-multiline, objects: always-multiline}] 26 | comma-spacing: [2, {before: false, after: true}] 27 | comma-style: [2, last] 28 | complexity: [0] 29 | computed-property-spacing: [2, never] 30 | consistent-return: [0] 31 | constructor-super: [2] 32 | curly: [0, multi-or-nest] 33 | default-case: [0] 34 | dot-location: [2, property] 35 | dot-notation: [2] 36 | eol-last: [2] 37 | eqeqeq: [1, always] 38 | func-call-spacing: [2, never] 39 | func-name-matching: [0] 40 | func-names: [0] 41 | generator-star-spacing: [2, before] 42 | global-require: [0] 43 | guard-for-in: [0] 44 | handle-callback-err: [2, ^(err|error)$] 45 | id-blacklist: [0] 46 | id-length: [0] 47 | id-match: [0] 48 | indent: [2, 2, {VariableDeclarator: 0, SwitchCase: 1, MemberExpression: off}] 49 | jsx-quotes: [0] 50 | key-spacing: [2, {mode: minimum}] 51 | keyword-spacing: [2] 52 | lines-around-comment: [0] 53 | lines-around-directive: [0] 54 | max-len: [2, { 55 | code: 100, 56 | ignoreComments: true, 57 | ignoreRegExpLiterals: true, 58 | ignorePattern: "\\burl\\(\\s*[\"']?data:" 59 | }] 60 | max-lines: [0] 61 | max-nested-callbacks: [0] 62 | max-params: [0] 63 | max-statements-per-line: [0] 64 | max-statements: [0] 65 | multiline-ternary: [0, always-multiline] 66 | new-cap: [0] 67 | new-parens: [2] 68 | newline-before-return: [0] 69 | newline-per-chained-call: [0] 70 | no-alert: [0] 71 | no-array-constructor: [0] 72 | no-bitwise: [0] 73 | no-caller: [2] 74 | no-case-declarations: [2] 75 | no-class-assign: [2] 76 | no-cond-assign: [2, except-parens] 77 | no-confusing-arrow: [0, {allowParens: true}] 78 | no-const-assign: [2] 79 | no-constant-condition: [0] 80 | no-continue: [0] 81 | no-control-regex: [0] 82 | no-debugger: [2] 83 | no-delete-var: [2] 84 | no-div-regex: [0] 85 | no-dupe-args: [2] 86 | no-dupe-class-members: [2] 87 | no-dupe-keys: [2] 88 | no-duplicate-case: [2] 89 | no-duplicate-imports: [2] 90 | no-else-return: [0] 91 | no-empty-character-class: [2] 92 | no-empty-function: [0] 93 | no-empty-pattern: [2] 94 | no-empty: [2, {allowEmptyCatch: true}] 95 | no-eq-null: [2] 96 | no-eval: [2] 97 | no-ex-assign: [2] 98 | no-extend-native: [2] 99 | no-extra-bind: [2] 100 | no-extra-boolean-cast: [2] 101 | no-extra-label: [0] 102 | no-extra-parens: [0] 103 | no-extra-semi: [2] 104 | no-fallthrough: [2, {commentPattern: fallthrough.*}] 105 | no-floating-decimal: [0] 106 | no-func-assign: [2] 107 | no-global-assign: [2] 108 | no-implicit-coercion: [0] 109 | no-implicit-globals: [0] 110 | no-implied-eval: [2] 111 | no-inline-comments: [0] 112 | no-inner-declarations: [2] 113 | no-invalid-regexp: [2] 114 | no-invalid-this: [0] 115 | no-irregular-whitespace: [2] 116 | no-iterator: [2] 117 | no-label-var: [2] 118 | no-labels: [2, {allowLoop: true}] 119 | no-lone-blocks: [2] 120 | no-lonely-if: [0] 121 | no-loop-func: [2] 122 | no-magic-numbers: [0] 123 | no-mixed-operators: [0] 124 | no-mixed-requires: [2, true] 125 | no-mixed-spaces-and-tabs: [2] 126 | no-multi-spaces: [2, {ignoreEOLComments: true}] 127 | no-multi-str: [2] 128 | no-multiple-empty-lines: [2, {max: 2, maxEOF: 0, maxBOF: 0}] 129 | no-native-reassign: [2] 130 | no-negated-condition: [0] 131 | no-negated-in-lhs: [2] 132 | no-nested-ternary: [0] 133 | no-new-func: [2] 134 | no-new-object: [2] 135 | no-new-require: [2] 136 | no-new-symbol: [2] 137 | no-new-wrappers: [2] 138 | no-new: [0] 139 | no-obj-calls: [2] 140 | no-octal-escape: [2] 141 | no-octal: [2] 142 | no-path-concat: [2] 143 | no-process-exit: [0] 144 | no-proto: [2] 145 | no-redeclare: [2] 146 | no-regex-spaces: [2] 147 | no-restricted-imports: [0] 148 | no-restricted-modules: [2, domain, freelist, smalloc, sys] 149 | no-restricted-syntax: [2, WithStatement] 150 | no-return-assign: [2, except-parens] 151 | no-return-await: [2] 152 | no-script-url: [2] 153 | no-self-assign: [2, {props: true}] 154 | no-self-compare: [2] 155 | no-sequences: [2] 156 | no-shadow-restricted-names: [2] 157 | no-shadow: [0] 158 | no-spaced-func: [2] 159 | no-sparse-arrays: [2] 160 | no-tabs: [2] 161 | no-template-curly-in-string: [2] 162 | no-this-before-super: [2] 163 | no-throw-literal: [0] 164 | no-trailing-spaces: [2] 165 | no-undef-init: [2] 166 | no-undef: [2] 167 | no-undefined: [0] 168 | no-underscore-dangle: [0] 169 | no-unexpected-multiline: [2] 170 | no-unmodified-loop-condition: [0] 171 | no-unneeded-ternary: [2] 172 | no-unreachable: [2] 173 | no-unsafe-finally: [2] 174 | no-unsafe-negation: [2] 175 | no-unused-expressions: [1, {allowShortCircuit: true, allowTernary: true}] 176 | no-unused-labels: [0] 177 | no-unused-vars: [1, { 178 | args: after-used, 179 | vars: local, 180 | ignoreRestSiblings: true, 181 | argsIgnorePattern: ^_|^e$ 182 | }] 183 | no-use-before-define: [2, {functions: false, classes: false}] 184 | no-useless-call: [2] 185 | no-useless-computed-key: [2] 186 | no-useless-concat: [0] 187 | no-useless-constructor: [2] 188 | no-useless-escape: [2] 189 | no-var: [0] 190 | no-warning-comments: [0] 191 | no-whitespace-before-property: [2] 192 | no-with: [2] 193 | nonblock-statement-body-position: 0 194 | object-curly-newline: [2, {multiline: true, consistent: true}] 195 | object-curly-spacing: [2, never] 196 | object-shorthand: [0] 197 | one-var-declaration-per-line: [1] 198 | one-var: [2, {initialized: never}] 199 | operator-assignment: [2, always] 200 | operator-linebreak: [2, after, overrides: {"?": ignore, ":": ignore, "&&": ignore, "||": ignore}] 201 | padded-blocks: [0] 202 | prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}] 203 | prefer-numeric-literals: [2] 204 | prefer-rest-params: [0] 205 | quote-props: [2, consistent] 206 | quotes: [1, single, avoid-escape] 207 | radix: [2, as-needed] 208 | require-jsdoc: [0] 209 | require-yield: [2] 210 | semi-spacing: [2, {before: false, after: true}] 211 | semi: [2, always] 212 | sort-imports: [0] 213 | sort-keys: [0] 214 | space-before-blocks: [2, always] 215 | space-before-function-paren: [2, {anonymous: always, asyncArrow: always, named: never}] 216 | space-in-parens: [2, never] 217 | space-infix-ops: [2] 218 | space-unary-ops: [2] 219 | spaced-comment: [0, always, {markers: ["!"]}] 220 | strict: [2, global] 221 | symbol-description: [2] 222 | template-curly-spacing: [2, never] 223 | unicode-bom: [2, never] 224 | use-isnan: [2] 225 | valid-typeof: [2] 226 | wrap-iife: [2, inside] 227 | yield-star-spacing: [2, {before: true, after: false}] 228 | yoda: [2, never] 229 | 230 | overrides: 231 | - files: 232 | - "bg/*.js" 233 | env: 234 | es6: true 235 | serviceworker: true 236 | webextensions: true 237 | parserOptions: 238 | sourceType: module 239 | -------------------------------------------------------------------------------- /content/show-info.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.INJECTED !== 1 && (() => { 4 | window.INJECTED = 1; 5 | 6 | const EXT_ID = chrome.runtime.id; 7 | /** @type {Map}*/ 8 | const id2info = new Map(); 9 | /** @type {WeakMap}*/ 10 | const img2info = new WeakMap(); 11 | const xo = new IntersectionObserver(onIntersect, {rootMargin: 0x1F_FFFF + 'px'}); 12 | let uiStyle, uiCss; 13 | 14 | dispatchEvent(new Event(EXT_ID)); 15 | addEventListener(EXT_ID, quitWhenOrphaned); 16 | chrome.runtime.onMessage.addListener(onMessage); 17 | 18 | function onMessage(msg, sender, sendResponse) { 19 | if (msg.css) 20 | uiCss = msg.css; 21 | if (msg.src || msg.link) { 22 | const info = find(msg); 23 | const isRemote = info && start(info); 24 | const id = isRemote && `${Math.random()}.${performance.now()}`; 25 | if (id) id2info.set(id, info); 26 | sendResponse(id ? {id, src: info.src} : {}); 27 | return; 28 | } 29 | if (msg.info) { 30 | const r = id2info.get(msg.id); 31 | id2info.delete(msg.id); 32 | if (r) renderFileMeta(Object.assign(msg.info, r)); 33 | } 34 | } 35 | 36 | /** @param {IntersectionObserverEntry[]} entries */ 37 | function onIntersect(entries) { 38 | for (const e of entries) { 39 | if (!e.isIntersecting) 40 | removeAll({img: e.target}); 41 | } 42 | } 43 | 44 | function quitWhenOrphaned() { 45 | try { 46 | chrome.i18n.getUILanguage(); 47 | return; 48 | } catch (e) {} 49 | xo.disconnect(); 50 | for (const el of document.getElementsByClassName(EXT_ID)) 51 | el.remove(); 52 | xo.disconnect(); 53 | removeEventListener(EXT_ID, quitWhenOrphaned); 54 | chrome.runtime.onMessage.removeListener(onMessage); 55 | } 56 | 57 | function find({src, link}) { 58 | const rxLast = /[^/]*\/?$/; 59 | const tail = src && (src.startsWith('data:') ? src : src.match(rxLast)[0]).slice(-500); 60 | const linkSel = link === location.href ? 'a' : link && `a[href$="${( 61 | !link.startsWith(location.origin) ? 62 | link.slice(link.indexOf('://') + 1) : 63 | link.match(rxLast)[0] 64 | ).slice(-500)}"]`; 65 | const sel = !src ? linkSel : 66 | `${link ? linkSel : ''} :-webkit-any([src$="${tail}"], [srcset*="${tail}"])`; 67 | const img = findClickedImage(src || link, sel, document); 68 | /** @namespace Info */ 69 | return img && { 70 | img, 71 | el: null, 72 | src: img.src || img.currentSrc, 73 | alt: (img.alt || '').trim(), 74 | title: (img.title || '').trim(), 75 | duration: img.duration, 76 | bounds: img.getBoundingClientRect(), 77 | w: img.naturalWidth || img.videoWidth, 78 | h: img.naturalHeight || img.videoHeight, 79 | }; 80 | } 81 | 82 | function findClickedImage(src, selector, root) { 83 | for (let el of root.querySelectorAll(selector)) { 84 | const tag = el.tagName; 85 | const elSrc = tag === 'A' ? el.href : el.currentSrc || el.src; 86 | if (src !== elSrc) 87 | continue; 88 | if (tag === 'A') { 89 | for (const img of el.querySelectorAll('img, video')) 90 | if (isInView(img)) 91 | return img; 92 | continue; 93 | } 94 | if (tag === 'SOURCE') 95 | el = el.closest('video, picture'); 96 | if (isInView(el)) 97 | return el; 98 | } 99 | const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); 100 | for (let el; (el = walker.nextNode());) 101 | if (el.shadowRoot && (el = findClickedImage(src, selector, el.shadowRoot))) 102 | return el; 103 | } 104 | 105 | function isInView(el) { 106 | const b = el.getBoundingClientRect(); 107 | return b.width && b.height && 108 | b.right > 0 && b.bottom > 0 && 109 | b.top < innerHeight && b.left < innerWidth; 110 | } 111 | 112 | function start(info) { 113 | removeAll(info); 114 | createUI(info); 115 | // get size/type 116 | const {src} = info; 117 | const isData = /^data:.*?base64/.test(src); 118 | if (isData) { 119 | info.type = src.split(/[/;]/, 2).pop().toUpperCase(); 120 | info.size = src.split(';').pop().length / 6 * 8 | 0; 121 | renderFileMeta(info); 122 | } 123 | const style = $make('style', ':host {}'); 124 | info.root.append(uiStyle || (uiStyle = $make('style', uiCss)), style); 125 | document.body.appendChild(info.el); 126 | info.style = style.sheet.cssRules[0].style; 127 | adjustUI(info); 128 | return !isData; 129 | } 130 | 131 | function createUI(info) { 132 | const {img, src, w, h, alt, title, bounds: {width: dw, height: dh}} = info; 133 | const isImage = img.localName === 'img'; 134 | const el = $make('div', {img, className: EXT_ID}); 135 | const root = el.attachShadow({mode: 'closed'}); 136 | root.append( 137 | $make('main', [ 138 | $make('div', { 139 | id: 'close', 140 | textContent: 'x', 141 | onclick: event => { 142 | event.preventDefault(); 143 | event.stopImmediatePropagation(); 144 | removeAll({img}); 145 | }, 146 | }), 147 | $make('table', [ 148 | ['location', [ 149 | ['a', { 150 | id: 'url', 151 | href: src, 152 | title: src, 153 | textContent: src, 154 | target: '_blank', 155 | rel: 'noopener noreferrer', 156 | }], 157 | ]], 158 | ['dimensions', [ 159 | ['b', w && h ? `${w} x ${h} px` : ''], 160 | ['i', dw && dh && dw !== w && dh !== h ? 161 | ` (${tl('scaledTo')} ${formatNumber(dw)} x ${formatNumber(dh)} px)` : 162 | ''], 163 | ]], 164 | ['fileType', [ 165 | ['b', {id: 'type'}], 166 | ['span', ' ' + tl(`type${isImage ? 'Image' : 'Video'}`)], 167 | ['span', isImage ? '' : `, ${formatDuration(info)}`], 168 | ]], 169 | ['fileSize', [ 170 | ['b', {id: 'size'}], 171 | ['i', {id: 'bytes'}], 172 | ]], 173 | alt && 174 | ['alt', [ 175 | ['div', {id: 'alt', textContent: alt}], 176 | ]], 177 | title && 178 | ['title', [ 179 | ['div', {id: 'title', textContent: title}], 180 | ]], 181 | ].map(([tlKey, children] = []) => 182 | tlKey && 183 | $make('tr', [ 184 | $make('td', tl(tlKey)), 185 | $make('td', children.map(data => $make(...data))), 186 | ])) 187 | ), 188 | ]) 189 | ); 190 | info.el = el; 191 | info.root = root; 192 | } 193 | 194 | function renderFileMeta({size, type, root}) { 195 | // size 196 | const elSize = root.getElementById('size'); 197 | if (size) { 198 | let unit; 199 | let n = size; 200 | for (unit of ['', 'kiB', 'MiB', 'GiB']) { 201 | if (n < 1024) 202 | break; 203 | n /= 1024; 204 | } 205 | const bytes = `${formatNumber(size)} ${tl('bytes')}`; 206 | if (!unit) { 207 | size = bytes; 208 | } else { 209 | size = `${formatNumber(n)} ${unit}`; 210 | root.getElementById('bytes').textContent = ` (${bytes})`; 211 | } 212 | elSize.textContent = size; 213 | } else { 214 | elSize.closest('tr').remove(); 215 | } 216 | 217 | // type 218 | const elType = root.getElementById('type'); 219 | type = (type || '').split('/', 2).pop().toUpperCase(); 220 | if (type && type !== 'HTML') 221 | elType.textContent = type; 222 | else 223 | elType.closest('tr').remove(); 224 | } 225 | 226 | function adjustUI(info) { 227 | const {el, img, bounds, style, root} = info; 228 | // set position 229 | const r1 = document.scrollingElement.getBoundingClientRect(); 230 | const r2 = document.body.getBoundingClientRect(); 231 | const maxW = Math.max(r1.right, r2.right, innerWidth); 232 | const maxH = Math.max(r1.bottom, r2.bottom, scrollY + innerHeight); 233 | const b = el.getBoundingClientRect(); 234 | const x = clamp(bounds.left, 10, Math.min(innerWidth, maxW) - b.width - 40); 235 | const y = clamp(bounds.bottom, 10, Math.min(innerHeight, maxH) - b.height - 10); 236 | style.setProperty('left', x + scrollX + 'px', 'important'); 237 | style.setProperty('top', y + scrollY + 'px', 'important'); 238 | // set auto-fadeout 239 | let fadeOutTimer; 240 | el.onmouseleave = () => { 241 | style.setProperty('transition-duration', '5s', 'important'); 242 | style.setProperty('opacity', '0', 'important'); 243 | fadeOutTimer = setTimeout(removeAll, 5e3, {img}); 244 | }; 245 | el.onmouseenter = () => { 246 | clearTimeout(fadeOutTimer); 247 | style.setProperty('opacity', 1, 'important'); 248 | style.setProperty('transition-duration', '.15s', 'important'); 249 | }; 250 | if (!el.matches(':hover')) { 251 | el.onmouseenter(); 252 | fadeOutTimer = setTimeout(el.onmouseleave, 5e3); 253 | } 254 | // expand URL width to fill the entire cell 255 | requestAnimationFrame(() => { 256 | const elUrl = root.getElementById('url'); 257 | elUrl.style.maxWidth = elUrl.parentNode.offsetWidth + 'px'; 258 | }); 259 | xo.observe(img); 260 | img2info.set(img, info); 261 | } 262 | 263 | function removeAll({img} = {}) { 264 | const wasShown = img2info.size; 265 | const infos = img ? [img2info.get(img)].filter(Boolean) : img2info.values(); 266 | for (const i of infos) { 267 | i.el.remove(); 268 | xo.unobserve(i.img); 269 | img2info.delete(i.img); 270 | } 271 | if (wasShown && !img2info.size) 272 | xo.disconnect(); 273 | } 274 | 275 | function formatNumber(n) { 276 | return Number(n).toLocaleString(undefined, {maximumFractionDigits: 1}); 277 | } 278 | 279 | function formatDuration({duration}) { 280 | if (duration < 1) 281 | return '0:0' + duration.toFixed(2); 282 | return new Date(0, 0, 0, 0, 0, Math.round(Number(duration)) | 0) 283 | .toLocaleTimeString(undefined, {hourCycle: 'h24'}) 284 | // strip 00:0 at the beginning but leave one 0 for minutes so it looks like 0:07 285 | .replace(/^0+:0?/, ''); 286 | } 287 | 288 | function tl(s) { 289 | return chrome.i18n.getMessage(s); 290 | } 291 | 292 | function clamp(v, min, max) { 293 | return v < min ? min : v > max ? max : v; 294 | } 295 | 296 | function $make(tag, props) { 297 | const el = document.createElement(tag); 298 | if (typeof props === 'string') 299 | props = {textContent: props}; 300 | const hasProps = props && !Array.isArray(props); 301 | const children = hasProps ? props.children : props; 302 | if (children) 303 | el.append(...children.filter(Boolean)); 304 | if (children && hasProps) 305 | delete props.children; 306 | if (hasProps) 307 | Object.assign(el, props); 308 | return el; 309 | } 310 | })(); 311 | --------------------------------------------------------------------------------