├── .all-contributorsrc ├── .eslintignore ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js └── webpack.config.prod.js ├── package-lock.json ├── package.json ├── public ├── about.html ├── background.js ├── capture.html ├── favicon.ico ├── icon-128.png ├── icon-16.png ├── icon-48.png ├── icon-512.png ├── index.html └── manifest.json ├── resources └── images │ ├── demo-1.gif │ ├── icon-512.png │ ├── ss-1.png │ └── ss-2.png ├── scripts ├── build.js └── test.js └── src ├── AboutPage.js ├── Canvas.js ├── CapturePage.js ├── assets ├── fonts │ ├── Lato-Heavy.ttf │ ├── Lato-Regular.ttf │ ├── SourceCodePro-Bold.ttf │ ├── SourceCodePro-Regular.ttf │ └── SourceCodePro-SemiBold.ttf └── images │ ├── Ko-fi_Logo_RGB.png │ ├── SupportMe_blue@2x.png │ ├── about.svg │ ├── camera-black.svg │ ├── camera-white.svg │ ├── download-white.svg │ ├── eraser-black.svg │ ├── eraser-white.svg │ ├── exit-black.svg │ ├── exit-white.svg │ ├── file-download-black.svg │ ├── file-download-white.svg │ ├── pencil-black.svg │ ├── pencil-white.svg │ ├── recycle-bin-white.svg │ ├── recycle-bin.svg │ ├── redo-arrow-grey.svg │ ├── redo-arrow-white.svg │ ├── share-black.svg │ ├── share-white.svg │ ├── text-tool-black.svg │ ├── text-tool-white.svg │ ├── undo-arrow-grey.svg │ └── undo-arrow-white.svg ├── chrome ├── about.js ├── capture.js ├── main.js └── theme.js ├── components ├── Header │ └── Header.js ├── TextBox │ └── TextBox.js └── Toolbox │ └── Toolbox.js └── constants ├── theme.js └── values.js /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "guptayash", 10 | "name": "Yash Gupta", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/21957571?v=4", 12 | "profile": "https://www.linkedin.com/in/yash-gupta-aa2b6b148", 13 | "contributions": [ 14 | "code" 15 | ] 16 | } 17 | ], 18 | "contributorsPerLine": 7, 19 | "projectName": "blackboard", 20 | "projectOwner": "AshreneRoy", 21 | "repoType": "github", 22 | "repoHost": "https://github.com", 23 | "skipCi": true 24 | } 25 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/* 2 | config/* 3 | scripts/* -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "webextensions": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "ecmaVersion": 12, 16 | "sourceType": "module" 17 | }, 18 | "plugins": [ 19 | "react" 20 | ], 21 | "rules": { 22 | "react/jsx-uses-react": "error", 23 | "react/jsx-uses-vars": "error", 24 | "indent": ["error", 2], 25 | "semi": ["error", "always"], 26 | "prefer-arrow-callback": "error" 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.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 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | .history/ 23 | 24 | chrome-assets 25 | releases 26 | 27 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ashreneroy@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ashrene Roy 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 | # Blackboard 2 | 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) 5 | 6 | 7 | ## [Chrome extension](https://chrome.google.com/webstore/detail/blackboard/mjpaeljbciakgnigdligmdihfhnpbfla) to annotate webpages, capture and save full page screenshots 🚀 8 | 9 |
10 | logo 11 | 12 | ## Features 13 | - ✏️ Pencil tool 14 | - 📷 Full page screenshot 15 | - ✨ Textbox 16 | - 🎚️ Size adjustor for drawing/writing 17 | - ✨ Eraser tool 18 | - 🎨 Colour Palette 19 | - 🖌️ Colour Picker 20 | - 📥 Download screenshot 21 | - 🗑️ Reset 22 | 23 | ## Screenshots 24 | demo 25 | demo 26 | demo 27 | 28 | 29 | ## Installation 30 | Clone repo 31 | 32 | ``` 33 | git clone https://github.com/AshreneRoy/blackboard.git 34 | ``` 35 | Go to `blackboard` directory run 36 | 37 | ``` 38 | npm install 39 | ``` 40 | Now build the extension using 41 | ``` 42 | npm run build 43 | ``` 44 | You will see a `build` folder generated inside `blackboard` 45 | 46 | To avoid running `npm run build` after updating any file, you can run 47 | 48 | ``` 49 | npm run watch 50 | ``` 51 | 52 | which listens to any local file changes, and rebuilds automatically. 53 | 54 | ## Building 55 | ``` 56 | npm run build 57 | ``` 58 | 59 | ## Adding Blackboard extension to Chrome 60 | In Chrome browser, go to chrome://extensions page and switch on developer mode. This enables the ability to locally install a Chrome extension. 61 | 62 | Now click on the `LOAD UNPACKED` and browse to `blackboard/build` .This will install the Blackboard as a Chrome extension. 63 | 64 | ## License 65 | MIT 66 | 67 | ## Contributors ✨ 68 | 69 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |

Yash Gupta

💻
79 | 80 | 81 | 82 | 83 | 84 | 85 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 86 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. Variable expansion is supported in .env files. 31 | // https://github.com/motdotla/dotenv 32 | // https://github.com/motdotla/dotenv-expand 33 | dotenvFiles.forEach(dotenvFile => { 34 | if (fs.existsSync(dotenvFile)) { 35 | require('dotenv-expand')( 36 | require('dotenv').config({ 37 | path: dotenvFile, 38 | }) 39 | ); 40 | } 41 | }); 42 | 43 | // We support resolving modules according to `NODE_PATH`. 44 | // This lets you use absolute paths in imports inside large monorepos: 45 | // https://github.com/facebookincubator/create-react-app/issues/253. 46 | // It works similar to `NODE_PATH` in Node itself: 47 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 48 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 49 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 50 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 51 | // We also resolve them to make sure all tools using them work consistently. 52 | const appDirectory = fs.realpathSync(process.cwd()); 53 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 54 | .split(path.delimiter) 55 | .filter(folder => folder && !path.isAbsolute(folder)) 56 | .map(folder => path.resolve(appDirectory, folder)) 57 | .join(path.delimiter); 58 | 59 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 60 | // injected into the application via DefinePlugin in Webpack configuration. 61 | const REACT_APP = /^REACT_APP_/i; 62 | 63 | function getClientEnvironment(publicUrl) { 64 | const raw = Object.keys(process.env) 65 | .filter(key => REACT_APP.test(key)) 66 | .reduce( 67 | (env, key) => { 68 | env[key] = process.env[key]; 69 | return env; 70 | }, 71 | { 72 | // Useful for determining whether we’re running in production mode. 73 | // Most importantly, it switches React into the correct mode. 74 | NODE_ENV: process.env.NODE_ENV || 'development', 75 | // Useful for resolving the correct path to static assets in `public`. 76 | // For example, . 77 | // This should only be used as an escape hatch. Normally you would put 78 | // images into the `src` and `import` them in code to get their paths. 79 | PUBLIC_URL: publicUrl, 80 | } 81 | ); 82 | // Stringify all values so we can feed into Webpack DefinePlugin 83 | const stringified = { 84 | 'process.env': Object.keys(raw).reduce((env, key) => { 85 | env[key] = JSON.stringify(raw[key]); 86 | return env; 87 | }, {}), 88 | }; 89 | 90 | return { raw, stringified }; 91 | } 92 | 93 | module.exports = getClientEnvironment; 94 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right 17 | 18 | -------------------------------------------------------------------------------- /public/background.js: -------------------------------------------------------------------------------- 1 | // Called when the user clicks on the browser action 2 | chrome.action.onClicked.addListener(() => { 3 | // Send a message to the active tab 4 | chrome.tabs.query({active: true, currentWindow:true}, (tabs) => { 5 | let activeTab = tabs[0]; 6 | chrome.scripting.executeScript({ target: {tabId: activeTab.id}, files: ["static/js/main.js"] }, () => { 7 | if (chrome.runtime.lastError) { 8 | alert("Sorry this page is not accessible due to chrome web store policies"); 9 | return; 10 | } 11 | }); 12 | }); 13 | }); 14 | 15 | chrome.runtime.onMessage.addListener( 16 | (request, sender, sendResponse) => { 17 | switch (request.message) { 18 | case 'capture_screenshot': 19 | chrome.tabs.captureVisibleTab( 20 | null, {format: 'png'}, (dataURI) => { 21 | sendResponse(dataURI); 22 | }); 23 | break; 24 | case 'save': 25 | chrome.storage.local.set({image: request.image}, () => { 26 | chrome.tabs.create({ url: 'capture.html' }, () => { 27 | sendResponse('done'); 28 | }); 29 | }); 30 | break; 31 | default: 32 | console.log('Unmatched request of \'' + request + '\' from script to background.js from ' + sender); 33 | } 34 | return true; 35 | } 36 | ); 37 | 38 | chrome.runtime.onInstalled.addListener( 39 | (details) => { 40 | if(details.reason === 'install') { 41 | chrome.tabs.create({ url: 'about.html' }, () => {}); 42 | } 43 | } 44 | ); -------------------------------------------------------------------------------- /public/capture.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Blackboard 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/public/icon-128.png -------------------------------------------------------------------------------- /public/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/public/icon-16.png -------------------------------------------------------------------------------- /public/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/public/icon-48.png -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/public/icon-512.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Blackboard 8 | 9 | 10 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Blackboard", 3 | "name": "Blackboard", 4 | "version": "0.1.1", 5 | "description": "Draw over webpages, capture and save full page screenshots", 6 | "manifest_version": 3, 7 | "background": { 8 | "service_worker": "background.js" 9 | }, 10 | "permissions": [ "activeTab", "storage", "unlimitedStorage", "scripting"], 11 | "action": { 12 | "default_icon": "icon-48.png" 13 | }, 14 | "web_accessible_resources":[ 15 | { 16 | "resources": ["favicon.ico", "/static/media/*"], 17 | "matches": [""] 18 | } 19 | ], 20 | "icons": { 21 | "16": "icon-16.png", 22 | "48": "icon-48.png", 23 | "128": "icon-128.png" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/images/demo-1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/resources/images/demo-1.gif -------------------------------------------------------------------------------- /resources/images/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/resources/images/icon-512.png -------------------------------------------------------------------------------- /resources/images/ss-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/resources/images/ss-1.png -------------------------------------------------------------------------------- /resources/images/ss-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/resources/images/ss-2.png -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'production'; 5 | process.env.NODE_ENV = 'production'; 6 | 7 | // Makes the script crash on unhandled rejections instead of silently 8 | // ignoring them. In the future, promise rejections that are not handled will 9 | // terminate the Node.js process with a non-zero exit code. 10 | process.on('unhandledRejection', err => { 11 | throw err; 12 | }); 13 | 14 | // Ensure environment variables are read. 15 | require('../config/env'); 16 | 17 | const path = require('path'); 18 | const chalk = require('chalk'); 19 | const fs = require('fs-extra'); 20 | const webpack = require('webpack'); 21 | const config = require('../config/webpack.config.prod'); 22 | const paths = require('../config/paths'); 23 | const checkRequiredFiles = require('react-dev-utils/checkRequiredFiles'); 24 | const formatWebpackMessages = require('react-dev-utils/formatWebpackMessages'); 25 | const printHostingInstructions = require('react-dev-utils/printHostingInstructions'); 26 | const FileSizeReporter = require('react-dev-utils/FileSizeReporter'); 27 | const printBuildError = require('react-dev-utils/printBuildError'); 28 | 29 | const measureFileSizesBeforeBuild = 30 | FileSizeReporter.measureFileSizesBeforeBuild; 31 | const printFileSizesAfterBuild = FileSizeReporter.printFileSizesAfterBuild; 32 | const useYarn = fs.existsSync(paths.yarnLockFile); 33 | 34 | // These sizes are pretty large. We'll warn for bundles exceeding them. 35 | const WARN_AFTER_BUNDLE_GZIP_SIZE = 512 * 1024; 36 | const WARN_AFTER_CHUNK_GZIP_SIZE = 1024 * 1024; 37 | 38 | // Warn and crash if required files are missing 39 | if (!checkRequiredFiles([paths.appHtml])) { 40 | process.exit(1); 41 | } 42 | 43 | // First, read the current file sizes in build directory. 44 | // This lets us display how much they changed later. 45 | measureFileSizesBeforeBuild(paths.appBuild) 46 | .then(previousFileSizes => { 47 | // Remove all content but keep the directory so that 48 | // if you're in it, you don't end up in Trash 49 | fs.emptyDirSync(paths.appBuild); 50 | // Merge with the public folder 51 | copyPublicFolder(); 52 | // Start the webpack build 53 | return build(previousFileSizes); 54 | }) 55 | .then( 56 | ({ stats, previousFileSizes, warnings }) => { 57 | if (warnings.length) { 58 | console.log(chalk.yellow('Compiled with warnings.\n')); 59 | console.log(warnings.join('\n\n')); 60 | console.log( 61 | '\nSearch for the ' + 62 | chalk.underline(chalk.yellow('keywords')) + 63 | ' to learn more about each warning.' 64 | ); 65 | console.log( 66 | 'To ignore, add ' + 67 | chalk.cyan('// eslint-disable-next-line') + 68 | ' to the line before.\n' 69 | ); 70 | } else { 71 | console.log(chalk.green('Compiled successfully.\n')); 72 | } 73 | 74 | console.log('File sizes after gzip:\n'); 75 | printFileSizesAfterBuild( 76 | stats, 77 | previousFileSizes, 78 | paths.appBuild, 79 | WARN_AFTER_BUNDLE_GZIP_SIZE, 80 | WARN_AFTER_CHUNK_GZIP_SIZE 81 | ); 82 | console.log(); 83 | 84 | const appPackage = require(paths.appPackageJson); 85 | const publicUrl = paths.publicUrl; 86 | const publicPath = config.output.publicPath; 87 | const buildFolder = path.relative(process.cwd(), paths.appBuild); 88 | printHostingInstructions( 89 | appPackage, 90 | publicUrl, 91 | publicPath, 92 | buildFolder, 93 | useYarn 94 | ); 95 | }, 96 | err => { 97 | console.log(chalk.red('Failed to compile.\n')); 98 | printBuildError(err); 99 | process.exit(1); 100 | } 101 | ); 102 | 103 | // Create the production build and print the deployment instructions. 104 | function build(previousFileSizes) { 105 | console.log('Creating an optimized production build...'); 106 | 107 | let compiler = webpack(config); 108 | return new Promise((resolve, reject) => { 109 | compiler.run((err, stats) => { 110 | if (err) { 111 | return reject(err); 112 | } 113 | const messages = formatWebpackMessages(stats.toJson({}, true)); 114 | if (messages.errors.length) { 115 | // Only keep the first error. Others are often indicative 116 | // of the same problem, but confuse the reader with noise. 117 | if (messages.errors.length > 1) { 118 | messages.errors.length = 1; 119 | } 120 | return reject(new Error(messages.errors.join('\n\n'))); 121 | } 122 | if ( 123 | process.env.CI && 124 | (typeof process.env.CI !== 'string' || 125 | process.env.CI.toLowerCase() !== 'false') && 126 | messages.warnings.length 127 | ) { 128 | console.log( 129 | chalk.yellow( 130 | '\nTreating warnings as errors because process.env.CI = true.\n' + 131 | 'Most CI servers set it automatically.\n' 132 | ) 133 | ); 134 | return reject(new Error(messages.warnings.join('\n\n'))); 135 | } 136 | return resolve({ 137 | stats, 138 | previousFileSizes, 139 | warnings: messages.warnings, 140 | }); 141 | }); 142 | }); 143 | } 144 | 145 | function copyPublicFolder() { 146 | fs.copySync(paths.appPublic, paths.appBuild, { 147 | dereference: true, 148 | filter: file => file !== paths.appHtml, 149 | }); 150 | } 151 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | const jest = require('jest'); 19 | let argv = process.argv.slice(2); 20 | 21 | // Watch unless on CI or in coverage mode 22 | if (!process.env.CI && argv.indexOf('--coverage') < 0) { 23 | argv.push('--watch'); 24 | } 25 | 26 | 27 | jest.run(argv); 28 | -------------------------------------------------------------------------------- /src/AboutPage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import './assets/images/SupportMe_blue@2x.png'; 4 | 5 | const AboutPage = () => { 6 | const About = styled.div` 7 | padding-top: 90px; 8 | padding-left: 20%; 9 | padding-right: 20%; 10 | padding-bottom: 10%; 11 | margin: -8px; 12 | background-color: #EDF2F6; 13 | `; 14 | 15 | const Image = styled.img` 16 | width: 100%; 17 | box-shadow: 0 0 15px 0 rgb(0 0 0 / 40%); 18 | margin-bottom: 20px; 19 | `; 20 | 21 | const Box = styled.div` 22 | width: 100%; 23 | box-shadow: 0 0 15px 0 rgb(0 0 0 / 40%); 24 | text-align: justify; 25 | display: flex; 26 | justify-content: center; 27 | background-color: #FFFFFF; 28 | `; 29 | 30 | const Content = styled.div` 31 | width: 80%; 32 | padding-bottom: 80px; 33 | `; 34 | 35 | const HeaderContent = styled.header` 36 | background-color: #0E1218;; 37 | height: 60px; 38 | position: fixed; 39 | top: 0; 40 | right: 0; 41 | left: 0; 42 | display: flex; 43 | padding-left: 40px; 44 | padding-right: 30px; 45 | border-bottom: 1px solid black; 46 | justify-content: space-between; 47 | align-items: center; 48 | `; 49 | 50 | const Support = styled.img` 51 | width: 200px; 52 | `; 53 | 54 | const Heading = styled.div` 55 | display: flex; 56 | `; 57 | const Title = styled.h1` 58 | font-family: 'Lato Heavy'; 59 | color: white; 60 | font-size: 40px; 61 | font-weight:bold; 62 | `; 63 | const Letter = styled.h1` 64 | font-family: 'Lato Heavy'; 65 | color: #FFFFFF; 66 | font-size: 40px; 67 | font-weight:bold; 68 | border-radius: 8px; 69 | width: 45px; 70 | height: 45px; 71 | text-align: center; 72 | background: linear-gradient(red, blue); 73 | `; 74 | 75 | const H1 = styled.h1` 76 | font-family: 'Source Code Pro Bold'; 77 | font-size: 25px; 78 | padding-top: 30px; 79 | padding-bottom: 30px; 80 | `; 81 | 82 | const H2 = styled.h2` 83 | font-family: 'Source Code Pro Semi Bold'; 84 | font-size: 20px; 85 | `; 86 | 87 | const P = styled.p` 88 | font-family: 'Source Code Pro Regular'; 89 | font-size: 16px; 90 | `; 91 | 92 | const UL = styled.ul` 93 | font-family: 'Source Code Pro Regular'; 94 | list-style-type:none; 95 | font-size: 16px; 96 | padding-right: 20px; 97 | `; 98 | 99 | const Tutorial = styled.img` 100 | width: 800px; 101 | box-shadow: 0 0 15px 0 rgb(0 0 0 / 40%); 102 | margin: 20px 0 20px 0; 103 | `; 104 | 105 | const ThankYou = styled.p` 106 | font-family: 'Source Code Pro Semi Bold'; 107 | font-size: 20px; 108 | margin-top: 80px; 109 | `; 110 | 111 | return ( 112 |
113 | 114 | Blackboard 115 | window.open('https://ko-fi.com/ashrene', '_blank')}/> 116 | 117 | 118 | About Blackboard 119 | 120 | 121 |

👋 Hello there! Thanks for downloading Blackboard 💜

122 |

Use Blackboard to annotate live webpages and take full length screenshots.

123 |

🚀 Features:

124 |
    125 |
  • ✏️ Pencil tool
  • 126 |
  • 📷 Full page screenshot
  • 127 |
  • ✨ Textbox
  • 128 |
  • 🎚️ Size adjustor for drawing/writing
  • 129 |
  • ✨ Eraser tool
  • 130 |
  • 🎨 Colour Palette
  • 131 |
  • 🖌️ Colour Picker
  • 132 |
  • 📥 Download screenshot
  • 133 |
  • 🗑️ Reset
  • 134 |
135 |

📌 Stay tuned for more exciting features coming through! 🎁

136 |

📋 Quick tutorial:

137 |

👉 Click on pencil to free hand draw, eraser/trash/undo/redo for corrections, double-click anywhere on screen after selecting Textbox to insert Textbox.

138 | 139 |

👉 Take screenshots by clicking on the camera button. Wait for the page to be scrolled till the end to capture the full length. Avoid manually scrolling or changing tabs while the tool is capturing the screen.

140 | 141 |

👉 Drag the text box anywhere. Dragging is enabled when Textbox(T) is not selected.

142 | 143 |

👉 Resize by dragging bottom right corner. Resizing is enabled when Textbox(T) is selected.

144 | 145 |

If you like the tool please consider supporting the project. To donate just click on the Ko-fi button above ☝️ And do leave review on chrome webstore if you can :)

146 | Have a good day! 🍔🍟 147 |
148 |
149 |
150 |
151 | ); 152 | }; 153 | 154 | export default AboutPage; 155 | -------------------------------------------------------------------------------- /src/Canvas.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Stage, Layer, Line } from 'react-konva'; 3 | import styled from 'styled-components'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import Toolbox from './components/Toolbox/Toolbox'; 6 | import TextBox from './components/TextBox/TextBox'; 7 | import { TOOLBOX, ACTIONS, DEFAULT_STROKE_WIDTH, APP_ROOT_ID, APP_CANVAS_ID, APP_TOOLBOX_ID } from './constants/values'; 8 | import { DEFAULT_TOOL_COLOUR } from './constants/theme'; 9 | 10 | const CanvasMain = styled.div` 11 | position: absolute; 12 | top: 0px; 13 | right: 0px; 14 | left: 0px; 15 | z-index: 2147483647; 16 | background:none transparent; 17 | margin: 0; 18 | padding: 0; 19 | box-shadow: 0px 0px 0px 3px limegreen inset; 20 | `; 21 | 22 | const Canvas = () => { 23 | 24 | const [tool, setTool] = React.useState(TOOLBOX.PEN); 25 | const [lines, setLines] = React.useState([]); 26 | const [textBoxes, setTextBoxes] = React.useState([]); 27 | const [strokeWidth,setStrokeWidth] = React.useState(DEFAULT_STROKE_WIDTH); 28 | const [colourValue, setColourValue] = React.useState(DEFAULT_TOOL_COLOUR); 29 | const [isUndoDisabled, setUndoDisabled] = React.useState(false); 30 | const [isRedoDisabled, setRedoDisabled] = React.useState(false); 31 | 32 | const [undoStack, setUndoStack] = React.useState([]); 33 | const [redoStack, setRedoStack] = React.useState([]); 34 | const [undoEvent, setUndoEvent] = React.useState(); 35 | const [redoEvent, setRedoEvent] = React.useState(); 36 | 37 | const isStageListening = React.useRef(true); 38 | const isDrawing = React.useRef(false); 39 | const colorRef = React.useRef(); 40 | const textBoxRef = React.useRef(); 41 | const lineRef = React.useRef(); 42 | const undoStackRef = React.useRef(); 43 | const strokeWidthRef = React.useRef(); 44 | lineRef.current = lines; 45 | textBoxRef.current = textBoxes; 46 | colorRef.current = colourValue; 47 | undoStackRef.current = undoStack; 48 | strokeWidthRef.current = strokeWidth; 49 | 50 | let originalFixedTopElements = new Set(); 51 | let originalFixedBottomElements = new Set(); 52 | let canvas = document.createElement('canvas'); 53 | let originalBodyOverflowY = document.body.style.overflowY; 54 | let originalHTMLOverflow = document.documentElement.style.overflow; 55 | let originalScroll = document.documentElement.style.scrollBehavior; 56 | 57 | const memoTextBoxEvent = React.useCallback((e) => { 58 | let originalTextbox = textBoxRef.current; 59 | const textbox = { 60 | top: window.scrollY + e.clientY, 61 | left: e.clientX, 62 | color: colorRef.current, 63 | id: `blackboard-${uuidv4()}`, 64 | fontSize: strokeWidthRef.current 65 | }; 66 | originalTextbox.push(textbox); 67 | _push_to_stack(ACTIONS.CREATE_TEXTBOX, textbox); 68 | setTextBoxes(originalTextbox.concat()); 69 | }, []); 70 | 71 | React.useEffect(() => { 72 | if(tool === TOOLBOX.TEXTBOX) { 73 | window.addEventListener('dblclick', memoTextBoxEvent, true); 74 | } else { 75 | window.removeEventListener('dblclick', memoTextBoxEvent, true); 76 | } 77 | },[tool]); 78 | 79 | React.useEffect(() => { 80 | if(tool === TOOLBOX.PEN || tool === TOOLBOX.ERASER) { 81 | isStageListening.current = true; 82 | } else { 83 | isStageListening.current = false; 84 | } 85 | },[tool]); 86 | 87 | React.useEffect(() => { 88 | undoStack.length > 0 ? setUndoDisabled(false) : setUndoDisabled(true); 89 | redoStack.length > 0 ? setRedoDisabled(false) : setRedoDisabled(true); 90 | },[undoStack,redoStack]); 91 | 92 | //Hook to handle undo event 93 | React.useEffect(() => { 94 | if(undoEvent) { 95 | const length = undoStack.length; 96 | const stack = undoStack.slice(0, length - 1); 97 | setUndoStack(stack); 98 | setRedoStack([...redoStack, undoEvent]); 99 | if(undoEvent.action === ACTIONS.CREATE_LINE) { 100 | const originalLines = lines.slice(0, lines.length-1); 101 | setLines(originalLines); 102 | } 103 | if(undoEvent.action === ACTIONS.CREATE_TEXTBOX) { 104 | const originalTextboxes = textBoxes.slice(0, textBoxes.length-1); 105 | setTextBoxes(originalTextboxes); 106 | } 107 | if(undoEvent.action === ACTIONS.DELETE_TEXTBOX) { 108 | setTextBoxes([...textBoxes, undoEvent.data]); 109 | } 110 | if(undoEvent.action === ACTIONS.RESET) { 111 | setLines(undoEvent.data.lines); 112 | setTextBoxes(undoEvent.data.textBoxes); 113 | } 114 | } 115 | },[undoEvent]); 116 | 117 | //Hook to handle redo event 118 | React.useEffect(() => { 119 | if(redoEvent) { 120 | const length = redoStack.length; 121 | const stack = redoStack.slice(0, length - 1); 122 | setRedoStack(stack); 123 | setUndoStack([...undoStack, redoEvent]); 124 | if(redoEvent.action === ACTIONS.CREATE_LINE) { 125 | setLines([...lines, redoEvent.data]); 126 | } 127 | if(redoEvent.action === ACTIONS.CREATE_TEXTBOX) { 128 | setTextBoxes([...textBoxes, redoEvent.data]); 129 | } 130 | if(redoEvent.action === ACTIONS.DELETE_TEXTBOX) { 131 | const originalTextboxes = textBoxes.slice(0, textBoxes.length-1); 132 | setTextBoxes(originalTextboxes); 133 | } 134 | if(redoEvent.action === ACTIONS.RESET) { 135 | setLines([]); 136 | setTextBoxes([]); 137 | } 138 | } 139 | },[redoEvent]); 140 | 141 | const handleUndo = () => { 142 | if(undoStack.length > 0) { 143 | setTool(TOOLBOX.DEFAULT); 144 | const length = undoStack.length; 145 | const event = {...undoStack[length - 1]}; 146 | setUndoEvent(event); 147 | } 148 | }; 149 | 150 | const handleRedo = () => { 151 | if(redoStack.length > 0) { 152 | setTool(TOOLBOX.DEFAULT); 153 | const length = redoStack.length; 154 | const event = {...redoStack[length - 1]}; 155 | setRedoEvent(event); 156 | } 157 | }; 158 | 159 | // Line drawing events 160 | const handleMouseDown = (e) => { 161 | if(!isStageListening.current) { 162 | return; 163 | } 164 | isDrawing.current = true; 165 | const pos = e.target.getStage().getPointerPosition(); 166 | setLines([...lines, { tool: {name: tool, strokeWidth: strokeWidth, colour: colourValue}, points: [pos.x, pos.y] }]); 167 | }; 168 | 169 | const handleMouseMove = (e) => { 170 | // no drawing - skipping 171 | if(!isStageListening.current) { 172 | return; 173 | } 174 | if (!isDrawing.current) { 175 | return; 176 | } 177 | const stage = e.target.getStage(); 178 | const point = stage.getPointerPosition(); 179 | let newLine = lines; 180 | let size = lines.length - 1; 181 | let lastLine = lines[size]; 182 | // add point 183 | lastLine.points = lastLine.points.concat([point.x, point.y]); 184 | newLine[size] = lastLine; 185 | setLines(newLine.concat()); 186 | }; 187 | 188 | const handleMouseUp = () => { 189 | if(!isStageListening.current) { 190 | return; 191 | } 192 | _push_to_stack(ACTIONS.CREATE_LINE, lineRef.current[lineRef.current.length - 1]); 193 | isDrawing.current = false; 194 | }; 195 | 196 | // Screenshot event 197 | const handleCapture = () => { 198 | let app = document.getElementById(APP_CANVAS_ID); 199 | let top = app.getBoundingClientRect().top + window.pageYOffset; 200 | let height = app.getBoundingClientRect().height; 201 | _prepare(); 202 | 203 | let n = (height / (window.innerHeight - 80)); 204 | let screenshots = []; 205 | canvas.width = window.innerWidth; 206 | canvas.height = height; 207 | let context = canvas.getContext('2d'); 208 | for (let i = 0; i { 221 | let isComplete = (j-n <= 1 && j-n >= 0) ? true : false; 222 | if (!isComplete) window.scrollTo({top: screenshots[j].scrollTo}); 223 | _getAllFixedElements(); 224 | window.setTimeout(() => { 225 | if(!isComplete) { 226 | chrome.runtime.sendMessage({message: 'capture_screenshot'}, (captured) => { 227 | let dY = window.scrollY; 228 | _getAllFixedElements(); 229 | let image = new Image(); 230 | image.onload = () => { 231 | context.globalCompositeOperation='destination-over'; 232 | context.drawImage(image, 0, dY, window.innerWidth, window.innerHeight); 233 | }; 234 | image.src = captured; 235 | let k = j + 1; 236 | capture(k,n,screenshots,context); 237 | }); 238 | 239 | } else { 240 | chrome.runtime.sendMessage({message: 'save', image: canvas.toDataURL('image/png')}, () => { 241 | _cleanup(); 242 | }); 243 | } 244 | }, 150); 245 | }; 246 | 247 | const _cleanup = () => { 248 | for(let item of originalFixedTopElements) { 249 | item.element.style.position = item.style; 250 | } 251 | for(let item of originalFixedBottomElements) { 252 | item.element.style.display = item.style; 253 | } 254 | let toolbox = document.getElementById(APP_TOOLBOX_ID); 255 | toolbox.style.display = 'flex'; 256 | let app = document.getElementById(APP_CANVAS_ID); 257 | app.style.boxShadow = '0px 0px 0px 3px limegreen inset'; 258 | document.body.style.overflowY = originalBodyOverflowY; 259 | document.documentElement.style.overflow = originalHTMLOverflow; 260 | document.documentElement.style.scrollBehavior = originalScroll; 261 | }; 262 | 263 | const _getAllFixedElements = () => { 264 | let elems = document.body.getElementsByTagName('*'); 265 | let length = elems.length; 266 | for(let i = 0; i < length; i++) { 267 | let elemStyle = window.getComputedStyle(elems[i]); 268 | if(elemStyle.getPropertyValue('position') === 'fixed' || elemStyle.getPropertyValue('position') === 'sticky' ) { 269 | if(elems[i].getBoundingClientRect().top < window.innerHeight/2) { 270 | const originalStylePosition = elemStyle.getPropertyValue('position'); 271 | elems[i].style.position = 'absolute'; 272 | originalFixedTopElements.add({style: originalStylePosition, element: elems[i]}); 273 | } else { 274 | const originalStyleDisplay = elemStyle.getPropertyValue('display'); 275 | elems[i].style.display = 'none'; 276 | originalFixedBottomElements.add({style: originalStyleDisplay, element: elems[i]}); 277 | } 278 | } 279 | } 280 | }; 281 | 282 | const _prepare = () => { 283 | let toolbox = document.getElementById(APP_TOOLBOX_ID); 284 | let app = document.getElementById(APP_CANVAS_ID); 285 | toolbox.style.display = 'none'; 286 | app.style.boxShadow = 'none'; 287 | document.documentElement.style.scrollBehavior = 'auto'; 288 | if(window.getComputedStyle(document.body).getPropertyValue('overflow-y') !== 'overlay') { 289 | document.body.style.overflowY = 'visible'; 290 | document.documentElement.style.overflow = 'hidden'; 291 | } 292 | }; 293 | 294 | const handlePencilOption = (width) => { 295 | setStrokeWidth(width); 296 | }; 297 | 298 | const handleColourPalette = (value) => { 299 | setColourValue(value); 300 | }; 301 | 302 | // Erase all event 303 | const handleReset = () => { 304 | _push_to_stack(ACTIONS.RESET, {lines, textBoxes}); 305 | setLines([]); 306 | setTextBoxes([]); 307 | 308 | }; 309 | 310 | // Textbox events 311 | const handleTextboxDelete = (id) => { 312 | let updatedTextboxList = textBoxes.filter((textbox) => { 313 | if(textbox.id === id) { 314 | _push_to_stack(ACTIONS.DELETE_TEXTBOX,textbox); 315 | return false; 316 | } 317 | return true; 318 | }); 319 | 320 | setTextBoxes(updatedTextboxList); 321 | }; 322 | 323 | const handleTextChange = (id, text) => { 324 | let updatedTextboxList = textBoxes.map((textbox) => { 325 | if(textbox.id === id) { 326 | textbox.text = text; 327 | } 328 | return textbox; 329 | }); 330 | setTextBoxes(updatedTextboxList); 331 | }; 332 | 333 | const calculateHeight = () => { 334 | const bodyHeight = document.documentElement.scrollHeight; 335 | // use heightRef instead of height inside window eventlistener of useEffect : https://stackoverflow.com/questions/56511176/state-being-reset 336 | // MAX canvas length in chrome and firefox is around 32767 pixels 337 | if (bodyHeight < 8000) { 338 | return bodyHeight - 5; // Subtract few pixels to avoid extending body length 339 | } 340 | return 8000; 341 | }; 342 | 343 | const _push_to_stack = (action, data) => { 344 | let stack = { 345 | action, 346 | data, 347 | }; 348 | setUndoStack([...undoStackRef.current, stack]); 349 | setRedoStack([]); 350 | }; 351 | 352 | const handleAppClose = () => { 353 | const blackBoardApp = document.getElementById(APP_ROOT_ID); 354 | if(blackBoardApp) { 355 | blackBoardApp.parentNode.removeChild(blackBoardApp); 356 | } 357 | }; 358 | 359 | return ( 360 | 361 | 368 | 369 | {lines.map((line, i) => ( 370 | 381 | ))} 382 | 383 | 384 | { 385 | textBoxes.map((textbox) => ( 386 | handleTextboxDelete(textbox.id)} 396 | handleTextChange={handleTextChange} 397 | /> 398 | )) 399 | } 400 | { 402 | setTool(tool); 403 | }} 404 | tool={tool} 405 | handleCapture={handleCapture} 406 | handlePencilOption={handlePencilOption} 407 | handleColourPalette={handleColourPalette} 408 | handleReset={handleReset} 409 | handleUndo={handleUndo} 410 | handleRedo={handleRedo} 411 | handleAppClose={handleAppClose} 412 | strokeWidth={strokeWidth} 413 | colourValue={colourValue} 414 | isUndoDisabled={isUndoDisabled} 415 | isRedoDisabled={isRedoDisabled} 416 | > 417 | 418 | 419 | ); 420 | }; 421 | 422 | export default Canvas; 423 | -------------------------------------------------------------------------------- /src/CapturePage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import Header from './components/Header/Header'; 5 | import './assets/images/Ko-fi_Logo_RGB.png'; 6 | 7 | const Screenshot = styled.div` 8 | padding-top: 90px; 9 | padding-left: 10%; 10 | padding-right: 10%; 11 | padding-bottom: 10%; 12 | margin: -8px; 13 | background-color: #EDF2F6; 14 | `; 15 | 16 | const Image = styled.img` 17 | width: 100%; 18 | box-shadow: 0 0 15px 0 rgb(0 0 0 / 40%); 19 | `; 20 | 21 | const Footer = styled.footer` 22 | padding: 10px; 23 | margin: 0; 24 | font-size: 14px; 25 | display: flex; 26 | align-items: center; 27 | `; 28 | 29 | const Support = styled.img` 30 | width: 70px; 31 | `; 32 | 33 | const CapturePage = (props) => { 34 | 35 | const handleSave = (datauri) => { 36 | let image = datauri.replace("image/png", "image/octet-stream"); 37 | var link = document.createElement('a'); 38 | link.download = "screenshot-" + new Date().getTime() + ".png"; 39 | link.href = image; 40 | link.click(); 41 | }; 42 | 43 | return ( 44 |
45 |
{handleSave(props.image);}}>
46 | 47 |

💡 For best results avoid scrolling the page or changing tabs when screenshot is being captured

48 | Blackboard Screenshot 49 |
50 |
51 |

👋 Like the tool? Please consider supporting the project on  

52 | window.open('https://ko-fi.com/ashrene', "_blank")}> 53 |   ❤️ 54 |
55 |
56 | ); 57 | }; 58 | 59 | CapturePage.propTypes = { 60 | image: PropTypes.string, 61 | }; 62 | 63 | export default CapturePage; 64 | -------------------------------------------------------------------------------- /src/assets/fonts/Lato-Heavy.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/fonts/Lato-Heavy.ttf -------------------------------------------------------------------------------- /src/assets/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/SourceCodePro-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/fonts/SourceCodePro-Bold.ttf -------------------------------------------------------------------------------- /src/assets/fonts/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/fonts/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/SourceCodePro-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/fonts/SourceCodePro-SemiBold.ttf -------------------------------------------------------------------------------- /src/assets/images/Ko-fi_Logo_RGB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/images/Ko-fi_Logo_RGB.png -------------------------------------------------------------------------------- /src/assets/images/SupportMe_blue@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashrene-roy/blackboard/c602fd403c1a2e3a972a73353b1f22b8622f8ad9/src/assets/images/SupportMe_blue@2x.png -------------------------------------------------------------------------------- /src/assets/images/about.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/camera-black.svg: -------------------------------------------------------------------------------- 1 | black camera -------------------------------------------------------------------------------- /src/assets/images/camera-white.svg: -------------------------------------------------------------------------------- 1 | black camera -------------------------------------------------------------------------------- /src/assets/images/download-white.svg: -------------------------------------------------------------------------------- 1 | download-file-square-line -------------------------------------------------------------------------------- /src/assets/images/eraser-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/eraser-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/exit-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/exit-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/file-download-black.svg: -------------------------------------------------------------------------------- 1 | download-file -------------------------------------------------------------------------------- /src/assets/images/file-download-white.svg: -------------------------------------------------------------------------------- 1 | download-file -------------------------------------------------------------------------------- /src/assets/images/pencil-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/pencil-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/recycle-bin-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/recycle-bin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/redo-arrow-grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/redo-arrow-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/share-black.svg: -------------------------------------------------------------------------------- 1 | share -------------------------------------------------------------------------------- /src/assets/images/share-white.svg: -------------------------------------------------------------------------------- 1 | share -------------------------------------------------------------------------------- /src/assets/images/text-tool-black.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/text-tool-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/undo-arrow-grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/undo-arrow-white.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/chrome/about.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import AboutPage from '../AboutPage'; 4 | import GlobalStyle from './theme'; 5 | 6 | const App = () => ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | 13 | 14 | const app = document.getElementById('about-root'); 15 | ReactDOM.render(, app); -------------------------------------------------------------------------------- /src/chrome/capture.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import PropTypes from 'prop-types'; 4 | import CapturePage from '../CapturePage'; 5 | import GlobalStyle from './theme'; 6 | 7 | const App = (props) => ( 8 | <> 9 | 10 | 11 | 12 | ); 13 | 14 | chrome.storage.local.get("image", (data) => { 15 | const app = document.getElementById('capture-root'); 16 | ReactDOM.render(, app); 17 | }); 18 | 19 | App.propTypes = { 20 | data: PropTypes.shape({ 21 | image: PropTypes.string 22 | }) 23 | }; -------------------------------------------------------------------------------- /src/chrome/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Canvas from '../Canvas'; 4 | import { APP_ROOT_ID } from '../constants/values'; 5 | 6 | const blackBoardApp = document.getElementById(APP_ROOT_ID); 7 | if(blackBoardApp) { 8 | if(blackBoardApp.style.display === 'none') { 9 | blackBoardApp.style.display = 'block'; 10 | } else { 11 | blackBoardApp.style.display = 'none'; 12 | } 13 | } else { 14 | const app = document.createElement('div'); 15 | app.id = APP_ROOT_ID; 16 | document.body.appendChild(app); 17 | ReactDOM.render(, app); 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/chrome/theme.js: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from 'styled-components'; 2 | import LatoHeavyTTF from '../assets/fonts/Lato-Heavy.ttf'; 3 | import LatoRegularTTF from '../assets/fonts/Lato-Heavy.ttf'; 4 | import SourceCodeProRegular from '../assets/fonts/SourceCodePro-Regular.ttf'; 5 | import SourceCodeProSemiBold from '../assets/fonts/SourceCodePro-SemiBold.ttf'; 6 | import SourceCodeProBold from '../assets/fonts/SourceCodePro-Bold.ttf'; 7 | 8 | const GlobalStyle = createGlobalStyle` 9 | @font-face { 10 | font-family: 'Lato Heavy'; 11 | src: url(${LatoHeavyTTF}) format('truetype'); 12 | } 13 | @font-face { 14 | font-family: 'Lato Regular'; 15 | src: url(${LatoRegularTTF}) format('truetype'); 16 | } 17 | @font-face { 18 | font-family: 'Source Code Pro Regular'; 19 | src: url(${SourceCodeProRegular}) format('truetype'); 20 | } 21 | @font-face { 22 | font-family: 'Source Code Pro Semi Bold'; 23 | src: url(${SourceCodeProSemiBold}) format('truetype'); 24 | } 25 | @font-face { 26 | font-family: 'Source Code Pro Bold'; 27 | src: url(${SourceCodeProBold}) format('truetype'); 28 | } 29 | `; 30 | 31 | export default GlobalStyle; -------------------------------------------------------------------------------- /src/components/Header/Header.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import '../../assets/images/file-download-black.svg'; 5 | import '../../assets/images/share-black.svg'; 6 | import '../../assets/images/about.svg'; 7 | 8 | const HeaderContent = styled.header` 9 | background-color: #0E1218;; 10 | height: 60px; 11 | position: fixed; 12 | top: 0; 13 | right: 0; 14 | left: 0; 15 | display: flex; 16 | align-items: center; 17 | padding-left: 40px; 18 | padding-right: 30px; 19 | justify-content: space-between; 20 | border-bottom: 1px solid black; 21 | `; 22 | const Heading = styled.div` 23 | display: flex; 24 | `; 25 | const Title = styled.h1` 26 | font-family: 'Lato Heavy'; 27 | color: white; 28 | font-size: 40px; 29 | font-weight:bold; 30 | `; 31 | const Letter = styled.h1` 32 | font-family: 'Lato Heavy'; 33 | color: #FFFFFF; 34 | font-size: 40px; 35 | font-weight:bold; 36 | border-radius: 8px; 37 | width: 45px; 38 | height: 45px; 39 | text-align: center; 40 | background: linear-gradient(red, blue); 41 | `; 42 | 43 | const Button = styled.button` 44 | background-color: #EDF2F6; 45 | border-radius: 8px; 46 | color: #000000; 47 | border: 1px solid #0E1218; 48 | height: 40px; 49 | display: flex; 50 | justify-content: space-between; 51 | align-items: center; 52 | margin-right: 10px; 53 | `; 54 | 55 | const ButtonWrapper = styled.div` 56 | display: flex; 57 | `; 58 | const Icon = styled.img` 59 | height: 20px; 60 | width: 20px; 61 | `; 62 | 63 | const Header = (props) => { 64 | 65 | return ( 66 | 67 | Blackboard 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | Header.propTypes = { 77 | handleSave: PropTypes.func 78 | }; 79 | 80 | export default Header; 81 | -------------------------------------------------------------------------------- /src/components/TextBox/TextBox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import Draggable from 'react-draggable'; 4 | import PropTypes from 'prop-types'; 5 | import '../../assets/images/recycle-bin.svg'; 6 | 7 | const Textarea = styled.textarea` 8 | color: ${props => props.color} !important; 9 | font-size: ${props => Math.max(14, props.fontSize)}px !important; 10 | min-width: 300px; 11 | background: none !important; 12 | border: none !important; 13 | outline: none !important; 14 | &:hover{ 15 | border: 1px solid #00BFFF !important; 16 | border-radius: 5px !important; 17 | } 18 | &:focus{ 19 | border: 1px solid #00BFFF !important; 20 | border-radius: 5px !important; 21 | } 22 | ${Container}:hover & { 23 | border: 1px solid #00BFFF !important; 24 | border-radius: 5px !important; 25 | } 26 | `; 27 | 28 | const Container = styled.div` 29 | display: flex; 30 | flex-direction: column; 31 | position: absolute; 32 | top: ${props => props.top}px; 33 | left: ${props => props.left}px; 34 | `; 35 | 36 | const Button = styled.button` 37 | border: 0px; 38 | display: none; 39 | width: 20px; 40 | height: 20px; 41 | padding: 2px; 42 | ${Container}:hover & { 43 | background: #00BFFF; 44 | color: white; 45 | display: flex; 46 | align-self: flex-end; 47 | align-items: center; 48 | } 49 | `; 50 | 51 | const Icon = styled.img` 52 | height: 15px; 53 | width: 15px; 54 | margin: 0; 55 | padding: 0; 56 | `; 57 | 58 | const TextBox = (props) => { 59 | 60 | return ( 61 | 62 | 63 | 68 | 69 | 70 | 71 | ); 72 | }; 73 | 74 | TextBox.propTypes = { 75 | id: PropTypes.string, 76 | disabled: PropTypes.bool, 77 | top: PropTypes.number, 78 | left: PropTypes.number, 79 | fontSize: PropTypes.number, 80 | color: PropTypes.string, 81 | text: PropTypes.string, 82 | handleTextChange: PropTypes.func, 83 | handleTextboxDelete: PropTypes.func 84 | }; 85 | 86 | export default TextBox; 87 | -------------------------------------------------------------------------------- /src/components/Toolbox/Toolbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styled from 'styled-components'; 3 | import PropTypes from 'prop-types'; 4 | import { TOOLBOX, APP_TOOLBOX_ID } from '../../constants/values'; 5 | import '../../assets/images/exit-white.svg'; 6 | import '../../assets/images/pencil-black.svg'; 7 | import '../../assets/images/eraser-black.svg'; 8 | import '../../assets/images/camera-black.svg'; 9 | import '../../assets/images/pencil-white.svg'; 10 | import '../../assets/images/eraser-white.svg'; 11 | import '../../assets/images/camera-white.svg'; 12 | import '../../assets/images/text-tool-white.svg'; 13 | import '../../assets/images/text-tool-black.svg'; 14 | import '../../assets/images/undo-arrow-white.svg'; 15 | import '../../assets/images/redo-arrow-white.svg'; 16 | import '../../assets/images/undo-arrow-grey.svg'; 17 | import '../../assets/images/redo-arrow-grey.svg'; 18 | import '../../assets/images/recycle-bin-white.svg'; 19 | 20 | const Container = styled.div` 21 | display: flex; 22 | flex-direction: column; 23 | position: fixed; 24 | top: 0; 25 | right: 0; 26 | background-color: #000000; 27 | border-radius: 5px; 28 | border: 2px solid #000000; 29 | `; 30 | 31 | const Row = styled.div` 32 | display: flex; 33 | `; 34 | 35 | const Tools = styled.div` 36 | display: ${props => props.isCollapse ? 'none' : 'flex'}; 37 | flex-wrap: wrap; 38 | background-color: #000000; 39 | align-items: center; 40 | width: 470px; 41 | `; 42 | 43 | const CollapseButton = styled.button` 44 | background-color: #000000; 45 | width: 15px; 46 | border: 2px solid #17191D; 47 | border-radius: 5px; 48 | display: flex; 49 | align-items: center; 50 | color: #ffffff; 51 | justify-content: center; 52 | `; 53 | 54 | const Tool = styled.button` 55 | color: white; 56 | background-color: #17191D; 57 | border: 1px solid #26282A; 58 | border-radius: 5px; 59 | padding-top: 5px; 60 | padding-bottom: 0px; 61 | margin-left: 5px; 62 | margin-right: 5px; 63 | height: 40px; 64 | width: 40px; 65 | `; 66 | 67 | const Icon = styled.img` 68 | height: 30px; 69 | width: 30px; 70 | `; 71 | 72 | const StrokeOption = styled.div` 73 | display: flex; 74 | justify-content: space-between; 75 | width: 450px; 76 | margin-left: 10px; 77 | margin-top: 20px; 78 | margin-bottom: 20px; 79 | `; 80 | 81 | const Slider = styled.input` 82 | width: 100%; 83 | `; 84 | 85 | const ColourPalette = styled.input` 86 | height: 40px; 87 | width: 40px; 88 | margin-left: 5px; 89 | margin-right: 5px; 90 | border: none; 91 | padding: 0; 92 | `; 93 | 94 | const Label = styled.label` 95 | color: #FFFFFF; 96 | margin-right: 15px; 97 | `; 98 | 99 | const Info = styled.p` 100 | padding: 0; 101 | margin: 0; 102 | color: #ffffff; 103 | margin-left: 20px; 104 | `; 105 | 106 | const Toolbox = (props) => { 107 | 108 | const [selectedTool, setSelectedTool] = React.useState(props.tool); 109 | const [isCollapse, setCollapse] = React.useState(false); 110 | 111 | React.useEffect(() => { 112 | setSelectedTool(props.tool); 113 | }, [props.tool]); 114 | 115 | const handleCollapse = () => { 116 | const state = isCollapse; 117 | setCollapse(!state); 118 | }; 119 | 120 | const handleSelectedTool = (tool) => { 121 | setSelectedTool(tool); 122 | props.handleSetTool(tool); 123 | }; 124 | 125 | const handleslider = (e) => { 126 | props.handlePencilOption(parseInt(e.target.value)); 127 | }; 128 | 129 | const handleColourPalette = (e) => { 130 | props.handleColourPalette(e.target.value); 131 | }; 132 | 133 | const handleTextbox = (tool) => { 134 | setSelectedTool(tool); 135 | props.handleSetTool(tool); 136 | }; 137 | 138 | const selected = { 139 | backgroundColor: 'white', 140 | }; 141 | 142 | return ( 143 | 144 | 145 | {'>'} 146 | 147 | props.handleUndo()} disabled={props.isUndoDisabled}> 148 | { 149 | props.isUndoDisabled ? : 150 | 151 | } 152 | 153 | props.handleRedo()} disabled={props.isRedoDisabled}> 154 | { 155 | props.isRedoDisabled ? : 156 | 157 | } 158 | 159 | { 160 | selectedTool === TOOLBOX.PEN ? 161 | handleSelectedTool(TOOLBOX.PEN)} style={selected}> 162 | 163 | : 164 | handleSelectedTool(TOOLBOX.PEN)}> 165 | 166 | 167 | } 168 | 169 | { 170 | selectedTool === TOOLBOX.ERASER ? 171 | handleSelectedTool(TOOLBOX.PEN)} style={selected}> 172 | 173 | : 174 | handleSelectedTool(TOOLBOX.ERASER)}> 175 | 176 | 177 | } 178 | { 179 | selectedTool === TOOLBOX.TEXTBOX ? 180 | handleTextbox(TOOLBOX.PEN, true)} style={selected}> 181 | 182 | : 183 | handleTextbox(TOOLBOX.TEXTBOX, false)}> 184 | 185 | 186 | } 187 | props.handleCapture()}> 188 | 189 | 190 | props.handleReset()}> 191 | 192 | 193 | props.handleAppClose()}> 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | { 203 | selectedTool === TOOLBOX.TEXTBOX && !isCollapse ? Double-click anywhere to insert Textbox : null 204 | } 205 | 206 | ); 207 | }; 208 | 209 | Toolbox.propTypes = { 210 | isUndoDisabled: PropTypes.bool, 211 | colourValue: PropTypes.string, 212 | isRedoDisabled: PropTypes.bool, 213 | strokeWidth: PropTypes.number, 214 | tool: PropTypes.string, 215 | handlePencilOption: PropTypes.func, 216 | handleColourPalette: PropTypes.func, 217 | handleSetTool: PropTypes.func, 218 | handleUndo: PropTypes.func, 219 | handleRedo: PropTypes.func, 220 | handleAppClose: PropTypes.func, 221 | handleCapture: PropTypes.func, 222 | handleReset: PropTypes.func, 223 | 224 | }; 225 | 226 | 227 | export default Toolbox; 228 | -------------------------------------------------------------------------------- /src/constants/theme.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_TOOL_COLOUR = "#df4b26"; -------------------------------------------------------------------------------- /src/constants/values.js: -------------------------------------------------------------------------------- 1 | export const APP_ROOT_ID = 'blackboard-extension-root-1234'; 2 | export const APP_CANVAS_ID = 'blackboard-extension-canvas-1234'; 3 | export const APP_TOOLBOX_ID = 'blackboard-extension-toolbox-1234'; 4 | 5 | export const TOOLBOX = { 6 | DEFAULT: "NONE", 7 | PEN: "PEN", 8 | ERASER: "ERASER", 9 | TEXTBOX: "TEXTBOX", 10 | RESET: "RESET", 11 | UNDO: "UNDO", 12 | REDO: "REDO", 13 | COLOUR: "COLOUR", 14 | CAPTURE: "CAPTURE" 15 | }; 16 | 17 | export const ACTIONS = { 18 | CREATE_LINE: "CREATE_LINE", 19 | CREATE_TEXTBOX: "CREATE_TEXTBOX", 20 | DELETE_TEXTBOX: "DELETE_TEXTBOX", 21 | RESET: "RESET" 22 | }; 23 | 24 | export const DEFAULT_STROKE_WIDTH = 4; --------------------------------------------------------------------------------