├── .babelrc ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── preview ├── chrome-web-store-img.png ├── preview-original.png └── preview.png ├── src ├── assets │ └── img │ │ ├── cross-32.png │ │ ├── icon-128.png │ │ └── icon-34.png ├── manifest.json ├── pages │ ├── Background │ │ ├── image-clipper.js │ │ ├── index.html │ │ └── index.ts │ ├── Content │ │ ├── content.styles.css │ │ └── index.ts │ └── Options │ │ ├── Options.css │ │ ├── Options.tsx │ │ ├── index.css │ │ ├── index.html │ │ └── index.tsx └── shared │ └── defaults.ts ├── tsconfig.json ├── utils ├── build.js ├── env.js └── webserver.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | // "@babel/preset-env", 4 | // "@babel/preset-react" 5 | "react-app" 6 | ], 7 | "plugins": [ 8 | // "@babel/plugin-proposal-class-properties", 9 | "react-hot-loader/babel" 10 | ] 11 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-app", 3 | "globals": { 4 | "chrome": "readonly" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | # secrets 20 | secrets.*.js 21 | 22 | *.zip -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | image-clipper.js -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5", 4 | "requirePragma": false, 5 | "arrowParens": "always" 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Michael Xieyang Liu 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screenshot Browser Extension 2 | 3 | [](https://chrome.google.com/webstore/detail/screenshot-extension/hmkbkbpdnembpeadgpcmjekihjmckdjh) 4 | 5 | ![package.json version](https://img.shields.io/github/package-json/v/lxieyang/screenshot-extension/master) 6 | [![MIT License](https://img.shields.io/github/license/lxieyang/screenshot-extension)](LICENSE) 7 | 8 | ![last commit](https://img.shields.io/github/last-commit/lxieyang/screenshot-extension/master) 9 | ![commit freq](https://img.shields.io/github/commit-activity/w/lxieyang/screenshot-extension) 10 | 11 | 14 | 15 | Based on [Chrome Extension Boilerplate with React 17 and Webpack 5](https://github.com/lxieyang/chrome-extension-boilerplate-react) 16 | 17 | ![preview](preview/preview-original.png) 18 | 19 | ## To install 20 | 21 | ### From Chrome store 22 | 23 | [](https://chrome.google.com/webstore/detail/screenshot-extension/hmkbkbpdnembpeadgpcmjekihjmckdjh) 24 | 25 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/hmkbkbpdnembpeadgpcmjekihjmckdjh)](https://chrome.google.com/webstore/detail/screenshot-extension/hmkbkbpdnembpeadgpcmjekihjmckdjh) 26 | 27 | ### From Github Releases 28 | 29 | 1. Download the latest `build.zip` from the [Release](https://github.com/lxieyang/screenshot-extension/releases) page. Or click [here](https://github.com/lxieyang/screenshot-extension/releases/download/v1.0.0/build.zip). 30 | 2. Unzip 31 | 3. Go to `chrome://extensions/` (or `edge://extensions/` if you're using MS Edge) and enable `Developer mode`. 32 | 4. Click `Load Unpacked`, then select the unzipped folder (which contains a file called `manifest.json`). 33 | 34 | ### Build from the source 35 | 36 | 1. Clone this git repo 37 | 2. Run `npm install`. 38 | 3. Run `npm run build`, which will generate a `build` folder. 39 | 4. Go to `chrome://extensions/` (or `edge://extensions/` if you're using MS Edge) and enable `Developer mode`. 40 | 5. Click `Load Unpacked`, then select the `build` folder (which contains a file called `manifest.json`). 41 | 42 | ## To use 43 | 44 | - Hold the `Option/Alt` key and drag the mouse to create partial screenshots. 45 | - Click the extension icon to create full-page screenshots. 46 | 47 | --- 48 | 49 | Michael Xieyang Liu | [Website](https://lxieyang.github.io) 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screenshot-extension", 3 | "version": "1.0.0", 4 | "description": "A browser extension that takes screenshots", 5 | "license": "MIT", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/lxieyang/screenshot-extension.git" 9 | }, 10 | "scripts": { 11 | "build": "node utils/build.js", 12 | "start": "node utils/webserver.js", 13 | "release": "npm run build && bestzip build.zip build/*", 14 | "prettier": "prettier --write '**/*.{js,jsx,css,html}'" 15 | }, 16 | "dependencies": { 17 | "@hot-loader/react-dom": "^17.0.0", 18 | "@types/chrome": "0.0.125", 19 | "@types/react": "^16.9.53", 20 | "@types/react-dom": "^16.9.8", 21 | "react": "^17.0.1", 22 | "react-dom": "^17.0.1", 23 | "react-hot-loader": "^4.13.0" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.12.3", 27 | "@babel/plugin-proposal-class-properties": "^7.12.1", 28 | "@babel/preset-env": "^7.12.1", 29 | "@babel/preset-react": "^7.12.1", 30 | "babel-eslint": "^10.1.0", 31 | "babel-loader": "^8.1.0", 32 | "babel-preset-react-app": "^10.0.0", 33 | "bestzip": "^2.1.7", 34 | "clean-webpack-plugin": "^3.0.0", 35 | "copy-webpack-plugin": "^6.2.1", 36 | "css-loader": "^5.0.0", 37 | "eslint": "^7.12.1", 38 | "eslint-config-react-app": "^6.0.0", 39 | "eslint-plugin-flowtype": "^5.2.0", 40 | "eslint-plugin-import": "^2.22.1", 41 | "eslint-plugin-jsx-a11y": "^6.4.1", 42 | "eslint-plugin-react": "^7.21.5", 43 | "eslint-plugin-react-hooks": "^4.2.0", 44 | "file-loader": "^6.1.1", 45 | "fs-extra": "^9.0.1", 46 | "html-loader": "^1.3.2", 47 | "html-webpack-plugin": "^5.0.0-alpha.7", 48 | "node-sass": "^4.14.1", 49 | "prettier": "^2.1.2", 50 | "sass-loader": "^10.0.4", 51 | "source-map-loader": "^1.1.2", 52 | "style-loader": "^2.0.0", 53 | "terser-webpack-plugin": "^5.0.2", 54 | "ts-loader": "^8.0.7", 55 | "typescript": "^4.0.5", 56 | "webpack": "^5.2.0", 57 | "webpack-cli": "^4.1.0", 58 | "webpack-dev-server": "^3.11.0" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /preview/chrome-web-store-img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/preview/chrome-web-store-img.png -------------------------------------------------------------------------------- /preview/preview-original.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/preview/preview-original.png -------------------------------------------------------------------------------- /preview/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/preview/preview.png -------------------------------------------------------------------------------- /src/assets/img/cross-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/src/assets/img/cross-32.png -------------------------------------------------------------------------------- /src/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/src/assets/img/icon-128.png -------------------------------------------------------------------------------- /src/assets/img/icon-34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/src/assets/img/icon-34.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Screenshot Extension (Open Source)", 3 | "background": { 4 | "page": "background.html" 5 | }, 6 | "browser_action": { 7 | "default_icon": "icon-34.png" 8 | }, 9 | "options_ui": { 10 | "page": "options.html", 11 | "open_in_tab": false 12 | }, 13 | "icons": { 14 | "128": "icon-128.png" 15 | }, 16 | "permissions": ["tabs", "downloads", "", "storage", "activeTab"], 17 | "content_scripts": [ 18 | { 19 | "matches": ["http://*/*", "https://*/*", ""], 20 | "js": ["contentScript.bundle.js"], 21 | "css": ["content.styles.css"] 22 | } 23 | ], 24 | "web_accessible_resources": [ 25 | "content.styles.css", 26 | "icon-128.png", 27 | "icon-34.png", 28 | "cross-32.png" 29 | ], 30 | "manifest_version": 2, 31 | "content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'" 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/Background/image-clipper.js: -------------------------------------------------------------------------------- 1 | /* 2 | * image-clipper 0.4.4 3 | * Node.js module for clipping & cropping JPEG, PNG, WebP images purely using the native Canvas APIs, excellent compatibility with the Browser & Electron & NW.js (Node-webkit), itself doesn't relies on any image processing libraries. 4 | * https://github.com/superRaytin/image-clipper 5 | * 6 | * Copyright 2015, Leon Shi 7 | * Released under the MIT license. 8 | */ 9 | 10 | !function t(e,n,i){function r(a,s){if(!n[a]){if(!e[a]){var u="function"==typeof require&&require;if(!s&&u)return u(a,!0);if(o)return o(a,!0);throw new Error("Cannot find module '"+a+"'")}var f=n[a]={exports:{}};e[a][0].call(f.exports,function(t){var n=e[a][1][t];return r(n?n:t)},f,f.exports,t,e,n,i)}return n[a].exports}for(var o="function"==typeof require&&require,a=0;an?n:t0&&i.each(e,function(e,n){"undefined"!=typeof e&&(t[n]=e)})})},i.setter=function(t,e,n){var r=i.type(e);if("String"===r){if("undefined"==typeof t[e])throw new Error("Invalid configuration name.");if("undefined"==typeof n)throw new Error("Lack of a value corresponding to the name");"Object"===i.type(n)&&"Object"===i.type(t[e])?i.extend(t[e],n):t[e]=n}else{if("Object"!==r)throw new Error("Invalid arguments");n=e,i.extend(t,n)}},i.getImageFormat=function(t){var e=t.substr(t.lastIndexOf(".")+1,t.length);return e="jpg"===e?"jpeg":e,"image/"+e},i.upperCaseFirstLetter=function(t){return t.replace(t.charAt(0),function(t){return t.toUpperCase()})},e.exports=i}).call(this,t("pBGvAp"),"undefined"!=typeof self?self:"undefined"!=typeof window?window:{})},{pBGvAp:6}],5:[function(t,e,n){},{}],6:[function(t,e,n){function i(){}var r=e.exports={};r.nextTick=function(){var t="undefined"!=typeof window&&window.setImmediate,e="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(t)return function(t){return window.setImmediate(t)};if(e){var n=[];return window.addEventListener("message",function(t){var e=t.source;if((e===window||null===e)&&"process-tick"===t.data&&(t.stopPropagation(),n.length>0)){var i=n.shift();i()}},!0),function(t){n.push(t),window.postMessage("process-tick","*")}}return function(t){setTimeout(t,0)}}(),r.title="browser",r.browser=!0,r.env={},r.argv=[],r.on=i,r.addListener=i,r.once=i,r.off=i,r.removeListener=i,r.removeAllListeners=i,r.emit=i,r.binding=function(t){throw new Error("process.binding is not supported")},r.cwd=function(){return"/"},r.chdir=function(t){throw new Error("process.chdir is not supported")}},{}]},{},[2]); 11 | -------------------------------------------------------------------------------- /src/pages/Background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/pages/Background/index.ts: -------------------------------------------------------------------------------- 1 | import '../../assets/img/icon-34.png'; 2 | import '../../assets/img/icon-128.png'; 3 | import '../../assets/img/cross-32.png'; 4 | import { defaults } from '../../shared/defaults'; 5 | // @ts-ignore 6 | import imageClipper from './image-clipper'; 7 | 8 | interface ImageDimension { 9 | w: number; 10 | h: number; 11 | } 12 | 13 | chrome.storage.sync.set({ openInTab: defaults.openInTab }); 14 | chrome.storage.sync.set({ download: defaults.download }); 15 | 16 | const getImageDimensions = (file: string): Promise => { 17 | return new Promise(function (resolved, rejected) { 18 | var img = new Image(); 19 | img.onload = function () { 20 | resolved({ w: img.width, h: img.height }); 21 | }; 22 | img.src = file; 23 | }); 24 | }; 25 | 26 | chrome.browserAction.setTitle({ 27 | title: 28 | 'Hold the Option/Alt key and drag the mouse to create partial screenshots.\nClick the icon to create full-page screenshots.', 29 | }); 30 | 31 | chrome.browserAction.onClicked.addListener(function () { 32 | chrome.tabs.captureVisibleTab(function (screenshotUrl) { 33 | if (!screenshotUrl) { 34 | return; 35 | } 36 | chrome.storage.sync.get(['download', 'openInTab'], (result) => { 37 | // download image 38 | if (result.download) { 39 | chrome.downloads.download({ 40 | url: screenshotUrl, 41 | filename: `${new Date().getTime().toString()}.jpg`, 42 | }); 43 | } 44 | 45 | // see for yourself the screenshot during testing 46 | if (result.openInTab) { 47 | chrome.tabs.create({ 48 | url: screenshotUrl, 49 | }); 50 | } 51 | }); 52 | }); 53 | }); 54 | 55 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 56 | if (request.msg === 'SCREENSHOT_WITH_COORDINATES') { 57 | let rect = request.rect; 58 | let windowSize = request.windowSize; 59 | chrome.tabs.captureVisibleTab(function (screenshotUrl) { 60 | if (!screenshotUrl) { 61 | return; 62 | } 63 | getImageDimensions(screenshotUrl).then( 64 | (imageDimensions: ImageDimension) => { 65 | let scale = imageDimensions.w / windowSize.width; 66 | let x = Math.floor(rect.x * scale); 67 | let y = Math.floor(rect.y * scale); 68 | let width = Math.floor(rect.width * scale); 69 | let height = Math.floor(rect.height * scale); 70 | imageClipper(screenshotUrl, function () { 71 | // @ts-ignore 72 | this.crop(x, y, width, height).toDataURL((dataUrl: string) => { 73 | chrome.storage.sync.get(['download', 'openInTab'], (result) => { 74 | // download image 75 | if (result.download) { 76 | chrome.downloads.download({ 77 | url: dataUrl, 78 | filename: `${new Date().getTime().toString()}.jpg`, 79 | }); 80 | } 81 | 82 | // see for yourself the screenshot during testing 83 | if (result.openInTab) { 84 | chrome.tabs.create({ 85 | url: dataUrl, 86 | }); 87 | } 88 | }); 89 | 90 | // get dimensions 91 | // getImageDimensions(dataUrl).then((croppedImageDimensions) => { 92 | // let dimensions = { 93 | // trueWidth: croppedImageDimensions.w, 94 | // trueHeight: croppedImageDimensions.h, 95 | // rectWidth: rect.width, 96 | // rectHeight: rect.height, 97 | // rectX: rect.x, 98 | // rectY: rect.y, 99 | // }; 100 | // console.log(dimensions); 101 | // }); 102 | }); 103 | }); 104 | } 105 | ); 106 | }); 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /src/pages/Content/content.styles.css: -------------------------------------------------------------------------------- 1 | .no-select { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | -khtml-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | cursor: crosshair; 9 | } 10 | 11 | #screenshot-bbox { 12 | box-sizing: border-box; 13 | border: 2px dashed rgba(255, 72, 0, 0.829); 14 | background-color: rgba(182, 238, 144, 0.26); 15 | 16 | display: none; 17 | } 18 | 19 | #screenshot-bbox.active { 20 | display: block; 21 | } 22 | -------------------------------------------------------------------------------- /src/pages/Content/index.ts: -------------------------------------------------------------------------------- 1 | const bboxAnchor = document.body.appendChild(document.createElement('div')); 2 | bboxAnchor.id = 'screenshot-bbox'; 3 | bboxAnchor.style.zIndex = '9999999'; 4 | bboxAnchor.style.position = 'fixed'; 5 | bboxAnchor.style.top = '0px'; 6 | bboxAnchor.style.left = '0px'; 7 | bboxAnchor.style.width = `0px`; 8 | bboxAnchor.style.height = `0px`; 9 | 10 | let hotKeyIsDown = false, 11 | mouseIsDown = false; 12 | let startingX: number | undefined = undefined, 13 | startingY: number | undefined = undefined; 14 | 15 | const resetBBoxAnchor = () => { 16 | bboxAnchor.style.removeProperty('top'); 17 | bboxAnchor.style.removeProperty('left'); 18 | bboxAnchor.style.removeProperty('bottom'); 19 | bboxAnchor.style.removeProperty('right'); 20 | bboxAnchor.style.removeProperty('width'); 21 | bboxAnchor.style.removeProperty('height'); 22 | }; 23 | 24 | const resetEverything = () => { 25 | hotKeyIsDown = false; 26 | mouseIsDown = false; 27 | startingX = undefined; 28 | startingY = undefined; 29 | document.body.classList.remove('no-select'); 30 | // document.body.style.removeProperty('cursor'); 31 | resetBBoxAnchor(); 32 | bboxAnchor.classList.remove('active'); 33 | }; 34 | 35 | window.addEventListener('mousedown', (event) => { 36 | if (event.altKey) { 37 | // Alt / Option key is down 38 | document.body.classList.add('no-select'); 39 | // document.body.style.cursor = `url(${chrome.extension.getURL( 40 | // 'cross-32.png' 41 | // )}), auto`; 42 | 43 | mouseIsDown = true; 44 | startingX = event.clientX; 45 | startingY = event.clientY; 46 | 47 | bboxAnchor.style.top = `${startingY}px`; 48 | bboxAnchor.style.left = `${startingX}px`; 49 | bboxAnchor.style.width = `0px`; 50 | bboxAnchor.style.height = `0px`; 51 | 52 | bboxAnchor.classList.add('active'); 53 | } 54 | }); 55 | 56 | const rectifyCursorCoordinates = (clientX: number, clientY: number) => { 57 | let currX = clientX < 0 ? 0 : clientX, 58 | currY = clientY < 0 ? 0 : clientY; 59 | return [currX, currY]; 60 | }; 61 | 62 | window.addEventListener('mousemove', (event) => { 63 | if (mouseIsDown && startingX !== undefined && startingY !== undefined) { 64 | const [currX, currY] = rectifyCursorCoordinates( 65 | event.clientX, 66 | event.clientY 67 | ); 68 | 69 | const width = Math.abs(currX - startingX), 70 | height = Math.abs(currY - startingY); 71 | 72 | resetBBoxAnchor(); 73 | 74 | if (startingY <= currY && startingX <= currX) { 75 | bboxAnchor.style.top = `${startingY}px`; 76 | bboxAnchor.style.left = `${startingX}px`; 77 | } else if (startingY <= currY && startingX >= currX) { 78 | bboxAnchor.style.top = `${startingY}px`; 79 | bboxAnchor.style.right = `${document.body.clientWidth - startingX}px`; 80 | } else if (startingY >= currY && startingX <= currX) { 81 | bboxAnchor.style.bottom = `${window.innerHeight - startingY}px`; 82 | bboxAnchor.style.left = `${startingX}px`; 83 | } else if (startingY >= currY && startingX >= currX) { 84 | bboxAnchor.style.bottom = `${window.innerHeight - startingY}px`; 85 | bboxAnchor.style.right = `${document.body.clientWidth - startingX}px`; 86 | } 87 | 88 | bboxAnchor.style.width = `${width}px`; 89 | bboxAnchor.style.height = `${height}px`; 90 | } 91 | }); 92 | 93 | window.addEventListener('mouseup', (event) => { 94 | if (!mouseIsDown || startingX === undefined || startingY === undefined) { 95 | return; 96 | } 97 | 98 | const [currX, currY] = rectifyCursorCoordinates(event.clientX, event.clientY); 99 | const rect = { 100 | x: Math.min(startingX, currX), 101 | y: Math.min(startingY, currY), 102 | width: Math.abs(startingX - currX), 103 | height: Math.abs(startingY - currY), 104 | }; 105 | const windowSize = { width: window.innerWidth, height: window.innerHeight }; 106 | 107 | resetEverything(); 108 | 109 | // avoid screenshoting the bbox 110 | setTimeout(() => { 111 | chrome.runtime.sendMessage({ 112 | msg: 'SCREENSHOT_WITH_COORDINATES', 113 | rect, 114 | windowSize, 115 | }); 116 | }, 1); 117 | }); 118 | 119 | window.addEventListener('keydown', (event) => { 120 | if (event.key === 'Escape') { 121 | resetEverything(); 122 | } else if (event.altKey) { 123 | hotKeyIsDown = true; 124 | document.body.classList.add('no-select'); 125 | // document.body.style.cursor = `url(${chrome.extension.getURL( 126 | // 'cross-32.png' 127 | // )}), auto`; 128 | } 129 | }); 130 | 131 | window.addEventListener('keyup', (event) => { 132 | if (hotKeyIsDown && !event.altKey) { 133 | hotKeyIsDown = false; 134 | if (!mouseIsDown) { 135 | document.body.classList.remove('no-select'); 136 | // document.body.style.removeProperty('cursor'); 137 | } 138 | } 139 | }); 140 | -------------------------------------------------------------------------------- /src/pages/Options/Options.css: -------------------------------------------------------------------------------- 1 | .OptionEntry { 2 | display: flex; 3 | align-items: center; 4 | user-select: none; 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/Options/Options.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { defaults } from '../../shared/defaults'; 3 | import './Options.css'; 4 | 5 | const Options: React.FC = () => { 6 | const [openInTab, setOpenInTab] = useState(false); 7 | const [download, setDownload] = useState(false); 8 | 9 | useEffect(() => { 10 | chrome.storage.sync.get(['openInTab'], (result) => { 11 | if (result.openInTab === undefined) { 12 | chrome.storage.sync.set({ openInTab: defaults.openInTab }); 13 | } else { 14 | setOpenInTab(result.openInTab); 15 | } 16 | }); 17 | 18 | chrome.storage.sync.get(['download'], (result) => { 19 | if (result.download === undefined) { 20 | chrome.storage.sync.set({ download: defaults.download }); 21 | } else { 22 | setDownload(result.download); 23 | } 24 | }); 25 | }, []); 26 | 27 | const openInNewTabClickedHandler = (to: boolean) => { 28 | setOpenInTab(to); 29 | chrome.storage.sync.set({ openInTab: to }); 30 | }; 31 | 32 | const downloadClickedHandler = (to: boolean) => { 33 | setDownload(to); 34 | chrome.storage.sync.set({ download: to }); 35 | }; 36 | 37 | return ( 38 |
39 |
40 | openInNewTabClickedHandler(!openInTab)} 47 | /> 48 | 49 |
50 | 51 |
52 | downloadClickedHandler(!download)} 59 | /> 60 | 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default Options; 67 | -------------------------------------------------------------------------------- /src/pages/Options/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lxieyang/screenshot-extension/d010091a68b70da0f37b891a34f7cb5b5574e686/src/pages/Options/index.css -------------------------------------------------------------------------------- /src/pages/Options/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/pages/Options/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | 4 | import Options from './Options'; 5 | import './index.css'; 6 | 7 | render(, window.document.querySelector('#app-container')); 8 | -------------------------------------------------------------------------------- /src/shared/defaults.ts: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | openInTab: true, 3 | download: false, 4 | }; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": false, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "noEmit": false, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"], 19 | "exclude": ["build", "node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /utils/build.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'production'; 3 | process.env.NODE_ENV = 'production'; 4 | process.env.ASSET_PATH = '/'; 5 | 6 | var webpack = require('webpack'), 7 | config = require('../webpack.config'); 8 | 9 | delete config.chromeExtensionBoilerplate; 10 | 11 | config.mode = 'production'; 12 | 13 | webpack(config, function (err) { 14 | if (err) throw err; 15 | }); 16 | -------------------------------------------------------------------------------- /utils/env.js: -------------------------------------------------------------------------------- 1 | // tiny wrapper with default env vars 2 | module.exports = { 3 | NODE_ENV: process.env.NODE_ENV || 'development', 4 | PORT: process.env.PORT || 3000, 5 | }; 6 | -------------------------------------------------------------------------------- /utils/webserver.js: -------------------------------------------------------------------------------- 1 | // Do this as the first thing so that any code reading it knows the right env. 2 | process.env.BABEL_ENV = 'development'; 3 | process.env.NODE_ENV = 'development'; 4 | process.env.ASSET_PATH = '/'; 5 | 6 | var WebpackDevServer = require('webpack-dev-server'), 7 | webpack = require('webpack'), 8 | config = require('../webpack.config'), 9 | env = require('./env'), 10 | path = require('path'); 11 | 12 | var options = config.chromeExtensionBoilerplate || {}; 13 | var excludeEntriesToHotReload = options.notHotReload || []; 14 | 15 | for (var entryName in config.entry) { 16 | if (excludeEntriesToHotReload.indexOf(entryName) === -1) { 17 | config.entry[entryName] = [ 18 | 'webpack-dev-server/client?http://localhost:' + env.PORT, 19 | 'webpack/hot/dev-server', 20 | ].concat(config.entry[entryName]); 21 | } 22 | } 23 | 24 | config.plugins = [new webpack.HotModuleReplacementPlugin()].concat( 25 | config.plugins || [] 26 | ); 27 | 28 | delete config.chromeExtensionBoilerplate; 29 | 30 | var compiler = webpack(config); 31 | 32 | var server = new WebpackDevServer(compiler, { 33 | https: false, 34 | hot: true, 35 | injectClient: false, 36 | writeToDisk: true, 37 | port: env.PORT, 38 | contentBase: path.join(__dirname, '../build'), 39 | publicPath: `http://localhost:${env.PORT}`, 40 | headers: { 41 | 'Access-Control-Allow-Origin': '*', 42 | }, 43 | disableHostCheck: true, 44 | }); 45 | 46 | server.listen(env.PORT); 47 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'), 2 | path = require('path'), 3 | fileSystem = require('fs-extra'), 4 | env = require('./utils/env'), 5 | { CleanWebpackPlugin } = require('clean-webpack-plugin'), 6 | CopyWebpackPlugin = require('copy-webpack-plugin'), 7 | HtmlWebpackPlugin = require('html-webpack-plugin'), 8 | TerserPlugin = require('terser-webpack-plugin'); 9 | 10 | const ASSET_PATH = process.env.ASSET_PATH || '/'; 11 | 12 | var alias = { 13 | 'react-dom': '@hot-loader/react-dom', 14 | }; 15 | 16 | // load the secrets 17 | var secretsPath = path.join(__dirname, 'secrets.' + env.NODE_ENV + '.js'); 18 | 19 | var fileExtensions = [ 20 | 'jpg', 21 | 'jpeg', 22 | 'png', 23 | 'gif', 24 | 'eot', 25 | 'otf', 26 | 'svg', 27 | 'ttf', 28 | 'woff', 29 | 'woff2', 30 | ]; 31 | 32 | if (fileSystem.existsSync(secretsPath)) { 33 | alias['secrets'] = secretsPath; 34 | } 35 | 36 | var options = { 37 | mode: process.env.NODE_ENV || 'development', 38 | entry: { 39 | options: path.join(__dirname, 'src', 'pages', 'Options', 'index.tsx'), 40 | background: path.join(__dirname, 'src', 'pages', 'Background', 'index.ts'), 41 | contentScript: path.join(__dirname, 'src', 'pages', 'Content', 'index.ts'), 42 | }, 43 | chromeExtensionBoilerplate: { 44 | notHotReload: ['contentScript'], 45 | }, 46 | output: { 47 | path: path.resolve(__dirname, 'build'), 48 | filename: '[name].bundle.js', 49 | publicPath: ASSET_PATH, 50 | }, 51 | module: { 52 | rules: [ 53 | { 54 | // look for .css or .scss files 55 | test: /\.(css|scss)$/, 56 | // in the `src` directory 57 | use: [ 58 | { 59 | loader: 'style-loader', 60 | }, 61 | { 62 | loader: 'css-loader', 63 | }, 64 | { 65 | loader: 'sass-loader', 66 | options: { 67 | sourceMap: true, 68 | }, 69 | }, 70 | ], 71 | }, 72 | { 73 | test: new RegExp('.(' + fileExtensions.join('|') + ')$'), 74 | loader: 'file-loader', 75 | options: { 76 | name: '[name].[ext]', 77 | }, 78 | exclude: /node_modules/, 79 | }, 80 | { 81 | test: /\.html$/, 82 | loader: 'html-loader', 83 | exclude: /node_modules/, 84 | }, 85 | { test: /\.(ts|tsx)$/, loader: 'ts-loader', exclude: /node_modules/ }, 86 | { 87 | test: /\.(js|jsx)$/, 88 | use: [ 89 | { 90 | loader: 'source-map-loader', 91 | }, 92 | { 93 | loader: 'babel-loader', 94 | }, 95 | ], 96 | exclude: /node_modules/, 97 | }, 98 | ], 99 | }, 100 | resolve: { 101 | alias: alias, 102 | extensions: fileExtensions 103 | .map((extension) => '.' + extension) 104 | .concat(['.js', '.jsx', '.ts', '.tsx', '.css']), 105 | }, 106 | plugins: [ 107 | new webpack.ProgressPlugin(), 108 | // clean the build folder 109 | new CleanWebpackPlugin({ 110 | verbose: true, 111 | cleanStaleWebpackAssets: false, 112 | }), 113 | // expose and write the allowed env vars on the compiled bundle 114 | new webpack.EnvironmentPlugin(['NODE_ENV']), 115 | new CopyWebpackPlugin({ 116 | patterns: [ 117 | { 118 | from: 'src/manifest.json', 119 | to: path.join(__dirname, 'build'), 120 | force: true, 121 | transform: function (content, path) { 122 | // generates the manifest file using the package.json informations 123 | return Buffer.from( 124 | JSON.stringify({ 125 | description: process.env.npm_package_description, 126 | version: process.env.npm_package_version, 127 | ...JSON.parse(content.toString()), 128 | }) 129 | ); 130 | }, 131 | }, 132 | ], 133 | }), 134 | new CopyWebpackPlugin({ 135 | patterns: [ 136 | { 137 | from: 'src/pages/Content/content.styles.css', 138 | to: path.join(__dirname, 'build'), 139 | force: true, 140 | }, 141 | ], 142 | }), 143 | new HtmlWebpackPlugin({ 144 | template: path.join(__dirname, 'src', 'pages', 'Options', 'index.html'), 145 | filename: 'options.html', 146 | chunks: ['options'], 147 | cache: false, 148 | }), 149 | new HtmlWebpackPlugin({ 150 | template: path.join( 151 | __dirname, 152 | 'src', 153 | 'pages', 154 | 'Background', 155 | 'index.html' 156 | ), 157 | filename: 'background.html', 158 | chunks: ['background'], 159 | cache: false, 160 | }), 161 | ], 162 | infrastructureLogging: { 163 | level: 'info', 164 | }, 165 | }; 166 | 167 | if (env.NODE_ENV === 'development') { 168 | options.devtool = 'eval-cheap-module-source-map'; 169 | } else { 170 | options.optimization = { 171 | minimize: true, 172 | minimizer: [ 173 | new TerserPlugin({ 174 | extractComments: false, 175 | }), 176 | ], 177 | }; 178 | } 179 | 180 | module.exports = options; 181 | --------------------------------------------------------------------------------