├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── LICENSE ├── README.md ├── app ├── BaseComponent.js ├── components │ ├── Button.css │ ├── Button.js │ ├── ColorPicker.css │ ├── ColorPicker.js │ ├── Header.css │ ├── Header.js │ ├── Icon.css │ ├── Icon.js │ ├── Link.css │ ├── Link.js │ ├── NotificationBar.css │ ├── NotificationBar.js │ ├── Options │ │ ├── Config.js │ │ ├── Options.css │ │ └── Options.js │ ├── Play.css │ ├── Play.js │ ├── PlayContainer.js │ ├── Reset.js │ ├── ResetBar.css │ ├── ResetBar.js │ ├── Times.js │ ├── Toggle.css │ ├── ToggleDisplay.js │ └── Variants.js └── containers │ ├── App.css │ ├── App.js │ └── Root.js ├── chrome ├── assets │ ├── fonts │ │ └── chessglyph-regular.woff │ └── img │ │ ├── arrow-down-medium.png │ │ ├── arrow-down-small.png │ │ ├── home.png │ │ ├── icon-128.png │ │ ├── icon-16.png │ │ ├── icon-48.png │ │ ├── interface-icon.png │ │ ├── logo-black.svg │ │ ├── logo.png │ │ ├── logo.svg │ │ └── sprite.png ├── extension │ ├── background.js │ ├── handleLiveChanges.js │ ├── popup.css │ └── popup.js ├── getCurrentUser.js ├── injectScriptWithWindowAccess.js ├── manifest.build.json ├── manifest.buildFirefox.json ├── manifest.dev.json ├── manifest.devFirefox.json └── views │ ├── background.jade │ └── popup.jade ├── circle.yml ├── package.json ├── scripts ├── .eslintrc ├── build.js ├── buildFirefox.js ├── compress.js ├── dev.js ├── devFirefox.js └── tasks.js ├── webpack ├── customPublicPath.js ├── dev.config.base.js ├── dev.config.js ├── devFirefox.config.js ├── prod.config.base.js ├── prod.config.js ├── prodFirefox.config.js ├── replace │ ├── JsonpMainTemplate.runtime.js │ └── log-apply-result.js └── test.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["add-module-exports", "transform-decorators-legacy", "transform-runtime"], 4 | "env": { 5 | "test": { 6 | "plugins": [ 7 | ["webpack-loaders", { "config": "webpack/test.config.js", "verbose": false }] 8 | ] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | 16 | [*.py] 17 | indent_size = 4 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "globals": { 5 | "chrome": true 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true 10 | }, 11 | "rules": { 12 | "react/prefer-stateless-function": 0, 13 | "react/jsx-filename-extension": 0, 14 | "consistent-return": 0, 15 | "comma-dangle": 0, 16 | "spaced-comment": 0, 17 | "global-require": 0, 18 | "no-param-reassign": ["error", {"props": false}] 19 | }, 20 | "plugins": [ 21 | "react" 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | .idea 5 | *.swp 6 | 7 | build/ 8 | dev/ 9 | buildFirefox/ 10 | devFirefox/ 11 | 12 | *.zip 13 | *.crx 14 | *.pem 15 | update.xml 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.0 2 | 1. Definitions. 3 | 1.1. "Contributor" means each individual or entity that 4 | creates or contributes to the creation of Modifications. 5 | 1.2. "Contributor Version" means the combination of the 6 | Original Software, prior Modifications used by a 7 | Contributor (if any), and the Modifications made by that 8 | particular Contributor. 9 | 1.3. "Covered Software" means (a) the Original Software, or 10 | (b) Modifications, or (c) the combination of files 11 | containing Original Software with files containing 12 | Modifications, in each case including portions thereof. 13 | 1.4. "Executable" means the Covered Software in any form 14 | other than Source Code. 15 | 1.5. "Initial Developer" means the individual or entity 16 | that first makes Original Software available under this 17 | License. 18 | 1.6. "Larger Work" means a work which combines Covered 19 | Software or portions thereof with code not governed by the 20 | terms of this License. 21 | 1.7. "License" means this document. 22 | 1.8. "Licensable" means having the right to grant, to the 23 | maximum extent possible, whether at the time of the initial 24 | grant or subsequently acquired, any and all of the rights 25 | conveyed herein. 26 | 1.9. "Modifications" means the Source Code and Executable 27 | form of any of the following: 28 | A. Any file that results from an addition to, 29 | deletion from or modification of the contents of a 30 | file containing Original Software or previous 31 | Modifications; 32 | B. Any new file that contains any part of the 33 | Original Software or previous Modification; or 34 | C. Any new file that is contributed or otherwise made 35 | available under the terms of this License. 36 | 1.10. "Original Software" means the Source Code and 37 | Executable form of computer software code that is 38 | originally released under this License. 39 | 1.11. "Patent Claims" means any patent claim(s), now owned 40 | or hereafter acquired, including without limitation, 41 | method, process, and apparatus claims, in any patent 42 | Licensable by grantor. 43 | 1.12. "Source Code" means (a) the common form of computer 44 | software code in which modifications are made and (b) 45 | associated documentation included in or with such code. 46 | 1.13. "You" (or "Your") means an individual or a legal 47 | entity exercising rights under, and complying with all of 48 | the terms of, this License. For legal entities, "You" 49 | includes any entity which controls, is controlled by, or is 50 | under common control with You. For purposes of this 51 | definition, "control" means (a) the power, direct or 52 | indirect, to cause the direction or management of such 53 | entity, whether by contract or otherwise, or (b) ownership 54 | of more than fifty percent (50%) of the outstanding shares 55 | or beneficial ownership of such entity. 56 | 2. License Grants. 57 | 2.1. The Initial Developer Grant. 58 | Conditioned upon Your compliance with Section 3.1 below and 59 | subject to third party intellectual property claims, the 60 | Initial Developer hereby grants You a world-wide, 61 | royalty-free, non-exclusive license: 62 | (a) under intellectual property rights (other than 63 | patent or trademark) Licensable by Initial Developer, 64 | to use, reproduce, modify, display, perform, 65 | sublicense and distribute the Original Software (or 66 | portions thereof), with or without Modifications, 67 | and/or as part of a Larger Work; and 68 | (b) under Patent Claims infringed by the making, 69 | using or selling of Original Software, to make, have 70 | made, use, practice, sell, and offer for sale, and/or 71 | otherwise dispose of the Original Software (or 72 | portions thereof). 73 | (c) The licenses granted in Sections 2.1(a) and (b) 74 | are effective on the date Initial Developer first 75 | distributes or otherwise makes the Original Software 76 | available to a third party under the terms of this 77 | License. 78 | (d) Notwithstanding Section 2.1(b) above, no patent 79 | license is granted: (1) for code that You delete from 80 | the Original Software, or (2) for infringements 81 | caused by: (i) the modification of the Original 82 | Software, or (ii) the combination of the Original 83 | Software with other software or devices. 84 | 2.2. Contributor Grant. 85 | Conditioned upon Your compliance with Section 3.1 below and 86 | subject to third party intellectual property claims, each 87 | Contributor hereby grants You a world-wide, royalty-free, 88 | non-exclusive license: 89 | (a) under intellectual property rights (other than 90 | patent or trademark) Licensable by Contributor to 91 | use, reproduce, modify, display, perform, sublicense 92 | and distribute the Modifications created by such 93 | Contributor (or portions thereof), either on an 94 | unmodified basis, with other Modifications, as 95 | Covered Software and/or as part of a Larger Work; and 96 | (b) under Patent Claims infringed by the making, 97 | using, or selling of Modifications made by that 98 | Contributor either alone and/or in combination with 99 | its Contributor Version (or portions of such 100 | combination), to make, use, sell, offer for sale, 101 | have made, and/or otherwise dispose of: (1) 102 | Modifications made by that Contributor (or portions 103 | thereof); and (2) the combination of Modifications 104 | made by that Contributor with its Contributor Version 105 | (or portions of such combination). 106 | (c) The licenses granted in Sections 2.2(a) and 107 | 2.2(b) are effective on the date Contributor first 108 | distributes or otherwise makes the Modifications 109 | available to a third party. 110 | (d) Notwithstanding Section 2.2(b) above, no patent 111 | license is granted: (1) for any code that Contributor 112 | has deleted from the Contributor Version; (2) for 113 | infringements caused by: (i) third party 114 | modifications of Contributor Version, or (ii) the 115 | combination of Modifications made by that Contributor 116 | with other software (except as part of the 117 | Contributor Version) or other devices; or (3) under 118 | Patent Claims infringed by Covered Software in the 119 | absence of Modifications made by that Contributor. 120 | 3. Distribution Obligations. 121 | 3.1. Availability of Source Code. 122 | Any Covered Software that You distribute or otherwise make 123 | available in Executable form must also be made available in 124 | Source Code form and that Source Code form must be 125 | distributed only under the terms of this License. You must 126 | include a copy of this License with every copy of the 127 | Source Code form of the Covered Software You distribute or 128 | otherwise make available. You must inform recipients of any 129 | such Covered Software in Executable form as to how they can 130 | obtain such Covered Software in Source Code form in a 131 | reasonable manner on or through a medium customarily used 132 | for software exchange. 133 | 3.2. Modifications. 134 | The Modifications that You create or to which You 135 | contribute are governed by the terms of this License. You 136 | represent that You believe Your Modifications are Your 137 | original creation(s) and/or You have sufficient rights to 138 | grant the rights conveyed by this License. 139 | 3.3. Required Notices. 140 | You must include a notice in each of Your Modifications 141 | that identifies You as the Contributor of the Modification. 142 | You may not remove or alter any copyright, patent or 143 | trademark notices contained within the Covered Software, or 144 | any notices of licensing or any descriptive text giving 145 | attribution to any Contributor or the Initial Developer. 146 | 3.4. Application of Additional Terms. 147 | You may not offer or impose any terms on any Covered 148 | Software in Source Code form that alters or restricts the 149 | applicable version of this License or the recipients' 150 | rights hereunder. You may choose to offer, and to charge a 151 | fee for, warranty, support, indemnity or liability 152 | obligations to one or more recipients of Covered Software. 153 | However, you may do so only on Your own behalf, and not on 154 | behalf of the Initial Developer or any Contributor. You 155 | must make it absolutely clear that any such warranty, 156 | support, indemnity or liability obligation is offered by 157 | You alone, and You hereby agree to indemnify the Initial 158 | Developer and every Contributor for any liability incurred 159 | by the Initial Developer or such Contributor as a result of 160 | warranty, support, indemnity or liability terms You offer. 161 | 3.5. Distribution of Executable Versions. 162 | You may distribute the Executable form of the Covered 163 | Software under the terms of this License or under the terms 164 | of a license of Your choice, which may contain terms 165 | different from this License, provided that You are in 166 | compliance with the terms of this License and that the 167 | license for the Executable form does not attempt to limit 168 | or alter the recipient's rights in the Source Code form 169 | from the rights set forth in this License. If You 170 | distribute the Covered Software in Executable form under a 171 | different license, You must make it absolutely clear that 172 | any terms which differ from this License are offered by You 173 | alone, not by the Initial Developer or Contributor. You 174 | hereby agree to indemnify the Initial Developer and every 175 | Contributor for any liability incurred by the Initial 176 | Developer or such Contributor as a result of any such terms 177 | You offer. 178 | 3.6. Larger Works. 179 | You may create a Larger Work by combining Covered Software 180 | with other code not governed by the terms of this License 181 | and distribute the Larger Work as a single product. In such 182 | a case, You must make sure the requirements of this License 183 | are fulfilled for the Covered Software. 184 | 4. Versions of the License. 185 | 4.1. New Versions. 186 | Sun Microsystems, Inc. is the initial license steward and 187 | may publish revised and/or new versions of this License 188 | from time to time. Each version will be given a 189 | distinguishing version number. Except as provided in 190 | Section 4.3, no one other than the license steward has the 191 | right to modify this License. 192 | 4.2. Effect of New Versions. 193 | You may always continue to use, distribute or otherwise 194 | make the Covered Software available under the terms of the 195 | version of the License under which You originally received 196 | the Covered Software. If the Initial Developer includes a 197 | notice in the Original Software prohibiting it from being 198 | distributed or otherwise made available under any 199 | subsequent version of the License, You must distribute and 200 | make the Covered Software available under the terms of the 201 | version of the License under which You originally received 202 | the Covered Software. Otherwise, You may also choose to 203 | use, distribute or otherwise make the Covered Software 204 | available under the terms of any subsequent version of the 205 | License published by the license steward. 206 | 4.3. Modified Versions. 207 | When You are an Initial Developer and You want to create a 208 | new license for Your Original Software, You may create and 209 | use a modified version of this License if You: (a) rename 210 | the license and remove any references to the name of the 211 | license steward (except to note that the license differs 212 | from this License); and (b) otherwise make it clear that 213 | the license contains terms which differ from this License. 214 | 5. DISCLAIMER OF WARRANTY. 215 | COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" 216 | BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, 217 | INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED 218 | SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR 219 | PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND 220 | PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY 221 | COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE 222 | INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF 223 | ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF 224 | WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF 225 | ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS 226 | DISCLAIMER. 227 | 6. TERMINATION. 228 | 6.1. This License and the rights granted hereunder will 229 | terminate automatically if You fail to comply with terms 230 | herein and fail to cure such breach within 30 days of 231 | becoming aware of the breach. Provisions which, by their 232 | nature, must remain in effect beyond the termination of 233 | this License shall survive. 234 | 6.2. If You assert a patent infringement claim (excluding 235 | declaratory judgment actions) against Initial Developer or 236 | a Contributor (the Initial Developer or Contributor against 237 | whom You assert such claim is referred to as "Participant") 238 | alleging that the Participant Software (meaning the 239 | Contributor Version where the Participant is a Contributor 240 | or the Original Software where the Participant is the 241 | Initial Developer) directly or indirectly infringes any 242 | patent, then any and all rights granted directly or 243 | indirectly to You by such Participant, the Initial 244 | Developer (if the Initial Developer is not the Participant) 245 | and all Contributors under Sections 2.1 and/or 2.2 of this 246 | License shall, upon 60 days notice from Participant 247 | terminate prospectively and automatically at the expiration 248 | of such 60 day notice period, unless if within such 60 day 249 | period You withdraw Your claim with respect to the 250 | Participant Software against such Participant either 251 | unilaterally or pursuant to a written agreement with 252 | Participant. 253 | 6.3. In the event of termination under Sections 6.1 or 6.2 254 | above, all end user licenses that have been validly granted 255 | by You or any distributor hereunder prior to termination 256 | (excluding licenses granted to You by any distributor) 257 | shall survive termination. 258 | 7. LIMITATION OF LIABILITY. 259 | UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT 260 | (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE 261 | INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF 262 | COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE 263 | LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR 264 | CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT 265 | LIMITATION, DAMAGES FOR LOST PROFITS, LOSS OF GOODWILL, WORK 266 | STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER 267 | COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN 268 | INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF 269 | LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL 270 | INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT 271 | APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO 272 | NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR 273 | CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT 274 | APPLY TO YOU. 275 | 8. U.S. GOVERNMENT END USERS. 276 | The Covered Software is a "commercial item," as that term is 277 | defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial 278 | computer software" (as that term is defined at 48 C.F.R. ¤ 279 | 252.227-7014(a)(1)) and "commercial computer software 280 | documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 281 | 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 282 | through 227.7202-4 (June 1995), all U.S. Government End Users 283 | acquire Covered Software with only those rights set forth herein. 284 | This U.S. Government Rights clause is in lieu of, and supersedes, 285 | any other FAR, DFAR, or other clause or provision that addresses 286 | Government rights in computer software under this License. 287 | 9. MISCELLANEOUS. 288 | This License represents the complete agreement concerning subject 289 | matter hereof. If any provision of this License is held to be 290 | unenforceable, such provision shall be reformed only to the 291 | extent necessary to make it enforceable. This License shall be 292 | governed by the law of the jurisdiction specified in a notice 293 | contained within the Original Software (except to the extent 294 | applicable law, if any, provides otherwise), excluding such 295 | jurisdiction's conflict-of-law provisions. Any litigation 296 | relating to this License shall be subject to the jurisdiction of 297 | the courts located in the jurisdiction and venue specified in a 298 | notice contained within the Original Software, with the losing 299 | party responsible for costs, including, without limitation, court 300 | costs and reasonable attorneys' fees and expenses. The 301 | application of the United Nations Convention on Contracts for the 302 | International Sale of Goods is expressly excluded. Any law or 303 | regulation which provides that the language of a contract shall 304 | be construed against the drafter shall not apply to this License. 305 | You agree that You alone are responsible for compliance with the 306 | United States export administration regulations (and the export 307 | control laws and regulation of any other countries) when You use, 308 | distribute or otherwise make available any Covered Software. 309 | 10. RESPONSIBILITY FOR CLAIMS. 310 | As between Initial Developer and the Contributors, each party is 311 | responsible for claims and damages arising, directly or 312 | indirectly, out of its utilization of rights under this License 313 | and You agree to work with Initial Developer and Contributors to 314 | distribute such responsibility on an equitable basis. Nothing 315 | herein is intended or shall be deemed to constitute any admission 316 | of liability. 317 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](http://i.imgur.com/5ptPTMF.png) 2 | 3 | # Chess.com Browser Extension 4 | 5 | Do chess your way! Even though Chess.com is working hard to provide the best online experience for playing, learning, and sharing chess, we know everyone has different preferences and ideas. We want to encourage everyone to customize their online chess experience as much as they want! 6 | 7 | ## Download the extension for... 8 | - [Chrome](https://chrome.google.com/webstore/detail/chess-browser-extension/fcfojodfingmafbdmlekaaoogcfpjegg) 9 | - [Firefox](https://addons.mozilla.org/en-US/firefox/addon/chess-com-browser-extension) 10 | - Safari: (TBA) 11 | - Opera: (TBA) 12 | 13 | --- 14 | 15 | ### Developing 16 | 17 | Once you've cloned the repo, install the NPM modules: 18 | 19 | ``` 20 | yarn install 21 | ``` 22 | 23 | Then run the developer build: 24 | 25 | #### For Chrome 26 | 27 | ``` 28 | yarn run dev 29 | ``` 30 | 31 | Load the extension via [Chrome Apps & Extensions Developer Tool](https://chrome.google.com/webstore/detail/chrome-apps-extensions-de/ohmmkhmmmpcnpikjeljgnaoabkaalbgc?hl=en). Be sure to select the `dev` folder from the "Load unpacked..." step. 32 | 33 | #### For Firefox 34 | 35 | ``` 36 | yarn run devFirefox 37 | ``` 38 | 39 | Load the extension [doing these steps](https://github.com/ChessCom/browser-extension/pull/48#issuecomment-264218199). 40 | 41 | ### Contribute 42 | If you would like to contribute code, please submit a pull request. Meaningful contributors will get a free Diamond membership. 43 | 44 | If you would just like to submit an idea, please go here: [http://goo.gl/forms/AVaggClVWuIyP87k1](http://goo.gl/forms/AVaggClVWuIyP87k1) 45 | 46 | #### All contributions should: 47 | - Respect the community (nothing negative or trolling) 48 | - Respect the service (don’t burden servers, hack around premium access, link to other sites) 49 | - Pass our ES6 lint standards, which you can check with this command: 50 | 51 | ``` 52 | yarn run lint 53 | ``` 54 | 55 | 56 | ### Issues 57 | Check out our current outstanding issues [here](https://github.com/ChessCom/browser-extension/issues) 58 | 59 | ### Special Thanks 60 | Huge thanks to all of these people and all of this software: 61 | 62 | [Rish](https://github.com/rish) 63 | [Martyn Chamberlin](https://github.com/martynchamberlin) 64 | [Jhen-Jie Hong](https://github.com/jhen0409/react-chrome-extension-boilerplate) 65 | [Winner Crespo](https://github.com/wistcc) 66 | -------------------------------------------------------------------------------- /app/BaseComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function shallowComp(A, B) { 4 | if (A === B) { 5 | // They are the same object 6 | return true; 7 | } 8 | 9 | if (typeof A !== 'object' || A === null || 10 | typeof B !== 'object' || B === null) { 11 | // This is only meant for comparing objects 12 | // If they are not the function could have unintended behaviour 13 | return false; 14 | } 15 | 16 | // Check that keys are the same 17 | const keysA = Object.keys(A); 18 | const keysB = Object.keys(B); 19 | if (keysA.length !== keysB.length) { 20 | return false; 21 | } 22 | return keysA.every(key => { 23 | if (!Object.hasOwnProperty.call(B, key)) { 24 | return false; 25 | } 26 | return A[key] === B[key]; 27 | }); 28 | } 29 | 30 | export default class BaseComponent extends React.Component { 31 | constructor(props, context) { 32 | super(props, context); 33 | if (this.constructor === BaseComponent) { 34 | throw new TypeError('BaseComponent is abstract'); 35 | } 36 | } 37 | 38 | shouldComponentUpdate = (nextProps, nextState) => ( 39 | !shallowComp(this.props, nextProps) || 40 | !shallowComp(this.state, nextState) 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /app/components/Button.css: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | margin: 0 4px 3px 0; 4 | height: 35px; 5 | border-bottom: 1px solid #a7a6a2; 6 | border-radius: 3px; 7 | color: #666463; 8 | background: #dbd9d7; 9 | font-size: 13px; 10 | } 11 | 12 | .selected { 13 | background: #bfbeba; 14 | } 15 | 16 | .small { 17 | width: 64px; 18 | min-width: 64px; 19 | } 20 | 21 | .panel { 22 | width: 100%; 23 | border-radius: 0; 24 | border: none; 25 | text-align: left; 26 | padding: 12px 15px 12px 45px; 27 | height: auto; 28 | margin: 0; 29 | position: relative; 30 | } 31 | 32 | .panelButtonIcon { 33 | position: absolute; 34 | top: 1px; 35 | left: 15px; 36 | } 37 | 38 | .huge { 39 | height: 50px; 40 | font-size: 17px; 41 | width: 48%; 42 | line-height: 1; 43 | float: left; 44 | position: relative; 45 | } 46 | 47 | .huge:after { 48 | position: absolute; 49 | font-family: Chess; 50 | font-weight: 400; 51 | font-style: normal; 52 | speak: none; 53 | color: #8c8a88; 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | content: '\003F'; 57 | right: 15px; 58 | top: 16px; 59 | padding-left: 15px; 60 | } 61 | 62 | .huge:nth-child(2) { 63 | margin-right: 0; 64 | float: right; 65 | } 66 | 67 | .hugeButtonIcon { 68 | line-height: .8; 69 | } 70 | 71 | .btn.btn-arrow:after { 72 | position: relative; 73 | left: 5px; 74 | padding-left: 5px; 75 | font-family: Chess; 76 | font-weight: 400; 77 | font-style: normal; 78 | speak: none; 79 | color: #8c8a88; 80 | -webkit-font-smoothing: antialiased; 81 | -moz-osx-font-smoothing: grayscale; 82 | content: '\003F'; 83 | } 84 | 85 | .button:nth-child(4n+5) { 86 | margin-right: 0; 87 | } 88 | 89 | .button:hover { 90 | background: #bfbeba; 91 | color: #312e2b; 92 | } 93 | -------------------------------------------------------------------------------- /app/components/Button.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames/bind'; 3 | import style from '../components/Button.css'; 4 | import BaseComponent from '../BaseComponent'; 5 | 6 | const cx = classNames.bind(style); 7 | 8 | export default class Button extends BaseComponent { 9 | 10 | static propTypes = { 11 | onClick: PropTypes.func.isRequired, 12 | concern: PropTypes.object, 13 | children: PropTypes.any, 14 | className: PropTypes.any 15 | }; 16 | 17 | constructor(props) { 18 | super(props); 19 | this.onClick = this.onClick.bind(this); 20 | } 21 | 22 | onClick() { 23 | this.props.onClick(this.props.concern); 24 | } 25 | 26 | render() { 27 | const className = cx({ 28 | [this.props.className]: true, 29 | button: true 30 | }); 31 | return ( 32 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/components/ColorPicker.css: -------------------------------------------------------------------------------- 1 | .row { 2 | padding-bottom: 10px; 3 | } 4 | 5 | label { 6 | font-size: 0.8rem; 7 | line-height: 1.8rem; 8 | font-weight: 500; 9 | } 10 | 11 | .arrow { 12 | display: block; 13 | float: right; 14 | position: absolute; 15 | top: 40%; 16 | right: 10%; 17 | } 18 | 19 | .resetButton { 20 | cursor: pointer; 21 | float: right; 22 | margin-right: 5px; 23 | padding-top: 3px; 24 | } 25 | 26 | .row:hover .resetButton i { 27 | color: #8c8a88 !important; 28 | } 29 | -------------------------------------------------------------------------------- /app/components/ColorPicker.js: -------------------------------------------------------------------------------- 1 | /* eslint quote-props: 0 */ 2 | import React, { PropTypes } from 'react'; 3 | import { ChromePicker } from 'react-color'; 4 | import reactCSS from 'reactcss'; 5 | import style from './ColorPicker.css'; 6 | import BaseComponent from '../BaseComponent'; 7 | import Reset from './Reset'; 8 | 9 | export default class ColorPicker extends BaseComponent { 10 | 11 | static propTypes = { 12 | name: PropTypes.string.isRequired, 13 | selector: PropTypes.string.isRequired, 14 | property: PropTypes.string.isRequired 15 | }; 16 | 17 | constructor(props, context) { 18 | super(props, context); 19 | 20 | this.state = { 21 | update: 'style', 22 | selector: '', 23 | property: '', 24 | color: {}, 25 | displayColorPicker: false 26 | }; 27 | } 28 | 29 | componentDidMount() { 30 | this.addStorageListener(); 31 | } 32 | 33 | setDefaultState = () => { 34 | this.setState({ 35 | color: { 36 | r: '255', 37 | g: '255', 38 | b: '255', 39 | a: '1' 40 | } 41 | }); 42 | } 43 | 44 | setDefaultStateAndSave = () => { 45 | chrome.storage.local.set({ style: {} }); 46 | this.setDefaultState(); 47 | } 48 | 49 | isDefaultColor = color => Object.keys(color).length === 0 || 50 | (color.r === '255' && color.g === '255' && color.b === '255' && color.a === '1'); 51 | 52 | checkIfStorageAlreadyExists = (name) => { 53 | chrome.storage.local.get('style', result => { 54 | if (!{}.hasOwnProperty.call(result, 'style')) { 55 | return this.setDefaultStateAndSave(); 56 | } 57 | 58 | if ({}.hasOwnProperty.call(result.style, name)) { 59 | const storedColor = result.style[name].color; 60 | this.setState({ color: storedColor }); 61 | } 62 | }); 63 | } 64 | 65 | addStorageListener = () => { 66 | this.checkIfStorageAlreadyExists(this.props.name); 67 | 68 | // Reset color picker when reset button is hit 69 | chrome.storage.onChanged.addListener(changes => { 70 | try { 71 | const newValue = changes.style.newValue; 72 | 73 | if (Object.keys(newValue).length === 0 && newValue.constructor === Object) { 74 | this.setDefaultState(); 75 | } 76 | } catch (e) { 77 | this.checkIfStorageAlreadyExists(); 78 | } 79 | }); 80 | } 81 | 82 | handleClick = () => { 83 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 84 | }; 85 | 86 | handleClose = () => { 87 | this.setState({ displayColorPicker: false }); 88 | }; 89 | 90 | handleChange = (color) => { 91 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 92 | chrome.tabs.sendMessage(tabs[0].id, { 93 | update: 'style', 94 | color: color.rgb, 95 | selector: this.props.selector, 96 | property: this.props.property 97 | }); 98 | }); 99 | }; 100 | 101 | handleChangeComplete = (color) => { 102 | this.setState({ 103 | color: color.rgb, 104 | selector: this.props.selector, 105 | property: this.props.property 106 | }); 107 | const state = JSON.parse(JSON.stringify(this.state)); 108 | const name = this.props.name; 109 | 110 | chrome.storage.local.get('style', result => { 111 | if (Object.keys(result).length === 0 && result.constructor === Object) { 112 | chrome.storage.local.set({ style: {} }); 113 | result.style = {}; 114 | } 115 | result.style[name] = state; 116 | delete result.style[name].displayColorPicker; 117 | chrome.storage.local.set(result); 118 | }); 119 | }; 120 | 121 | render() { 122 | const colorpicker = this.props; 123 | const inputId = `${this.props.name}_input`; 124 | const color = this.state.color; 125 | const styles = reactCSS({ 126 | 'default': { 127 | color: { 128 | width: '18px', 129 | height: '18px', 130 | borderRadius: '2px', 131 | background: `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`, 132 | float: 'left', 133 | marginRight: '30px' 134 | }, 135 | swatch: { 136 | padding: '5px', 137 | background: '#fff', 138 | borderRadius: '1px', 139 | boxShadow: '0 0 0 1px rgba(0,0,0,.1)', 140 | display: 'inline-block', 141 | cursor: 'pointer', 142 | position: 'relative', 143 | 'float': 'right' 144 | }, 145 | popover: { 146 | position: 'absolute', 147 | zIndex: '2', 148 | right: '20px' 149 | }, 150 | picker: { 151 | position: 'relative', 152 | }, 153 | cover: { 154 | position: 'fixed', 155 | top: '0px', 156 | right: '0px', 157 | bottom: '0px', 158 | left: '0px', 159 | } 160 | } 161 | }); 162 | 163 | return ( 164 |
165 | 166 |
167 |
168 | 173 |
174 | {!this.isDefaultColor(color) ? 175 | 187 | : null} 188 | {this.state.displayColorPicker ? 189 |
190 |
191 |
192 | 197 |
198 |
199 | : null} 200 |
201 | ); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /app/components/Header.css: -------------------------------------------------------------------------------- 1 | .header { 2 | padding: 15px; 3 | height: 55px; 4 | font-size: 13px; 5 | overflow: hidden; 6 | } 7 | 8 | .header img { 9 | float: left; 10 | } 11 | 12 | .userInfo { 13 | float: right; 14 | color: #fff; 15 | } 16 | 17 | .btn { 18 | cursor: pointer; 19 | float: right; 20 | background: #e4902d; 21 | color: white; 22 | padding: 0 13px; 23 | border-radius: 3px; 24 | height: 25px; 25 | } 26 | 27 | .userInfo img { 28 | margin: 0 0 0 15px; 29 | float: right; 30 | } 31 | 32 | .username { 33 | margin: 4px 0 -4px; 34 | max-width: 140px; 35 | overflow: hidden; 36 | display: inline-block; 37 | text-overflow: ellipsis; 38 | opacity: .8; 39 | } 40 | 41 | .home { 42 | display: inline-block; 43 | margin: 0.5rem; 44 | } 45 | 46 | .home:hover { 47 | cursor: pointer; 48 | } 49 | -------------------------------------------------------------------------------- /app/components/Header.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import style from './Header.css'; 3 | import Link from './Link'; 4 | import BaseComponent from '../BaseComponent'; 5 | 6 | export default class Header extends BaseComponent { 7 | 8 | static propTypes = { 9 | user: PropTypes.object.isRequired, 10 | }; 11 | 12 | render() { 13 | let userInfo = (
); 14 | if (!this.props.user.loading) { 15 | if (this.props.user.loggedIn) { 16 | userInfo = ( 17 | 18 |
19 | {this.props.user.username} 20 | 21 |
22 | 23 | ); 24 | } else { 25 | userInfo = ( 26 | 27 | 28 | 29 | ); 30 | } 31 | } 32 | 33 | return ( 34 |
35 | 36 | 37 | 38 | { userInfo } 39 |
40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/components/Icon.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Chess'; 3 | src: url('../../chrome/assets/fonts/chessglyph-regular.woff'); 4 | } 5 | .icon { 6 | font-family: 'Chess'; 7 | font-weight: normal; 8 | font-style: normal; 9 | speak: none; 10 | text-align: center; 11 | color: #8c8a88; 12 | display: inline-block; 13 | -webkit-font-smoothing: antialiased; 14 | } 15 | 16 | .icon:hover { 17 | text-decoration: none 18 | } 19 | 20 | .icon-magnifying-glass { 21 | padding-left: 1px; 22 | margin-left: -1px 23 | } 24 | 25 | .icon-chess:before { 26 | content: '\0069' 27 | } 28 | 29 | .icon-960:before { 30 | content: '\010C' 31 | } 32 | 33 | .icon-chess960:before { 34 | content: '\010C' 35 | } 36 | 37 | .icon-threecheck:before { 38 | content: '\00CB' 39 | } 40 | 41 | .icon-kingofthehill:before { 42 | content: '\012A' 43 | } 44 | 45 | .icon-losers:before { 46 | content: '\012B' 47 | } 48 | 49 | .icon-crazyhouse:before { 50 | content: '\010E' 51 | } 52 | 53 | .icon-bughouse:before { 54 | content: '\011A' 55 | } 56 | 57 | .icon-nav-horizontal:before { 58 | content: '\0052' 59 | } 60 | 61 | .icon-nav-vertical:before { 62 | content: '\00E7' 63 | } 64 | 65 | .icon-nav-expanded:before { 66 | content: '\03E1' 67 | } 68 | 69 | .icon-nav-collapsed:before { 70 | content: '\03E0' 71 | } 72 | 73 | .icon-bug:before { 74 | content: '\00F5' 75 | } 76 | 77 | .icon-hourglass:before { 78 | content: '\03FA' 79 | } 80 | 81 | .icon-camera:before { 82 | content: '\0048' 83 | } 84 | 85 | .icon-camera-plus:before { 86 | content: '\03DF' 87 | } 88 | 89 | .icon-globe:before { 90 | content: '\004D' 91 | } 92 | 93 | .icon-order:before { 94 | content: '\004F' 95 | } 96 | 97 | .icon-flag:before { 98 | content: '\0059' 99 | } 100 | 101 | .icon-calendar:before { 102 | content: '\0061' 103 | } 104 | 105 | .icon-calendar-alt:before { 106 | content: '\0032' 107 | } 108 | 109 | .icon-daily:before { 110 | content: '\0032' 111 | } 112 | 113 | .icon-chat:before { 114 | content: '\0063' 115 | } 116 | 117 | .icon-chat-alt:before { 118 | content: '\007A' 119 | } 120 | 121 | .icon-chat-x:before { 122 | content: '\2044' 123 | } 124 | 125 | .icon-chess-book:before { 126 | content: '\006F' 127 | } 128 | 129 | .icon-filter:before { 130 | content: '\203A' 131 | } 132 | 133 | .icon-lock:before { 134 | content: '\0064' 135 | } 136 | 137 | .icon-inbox:before { 138 | content: '\0065' 139 | } 140 | 141 | .icon-lightbulb:before { 142 | content: '\0067' 143 | } 144 | 145 | .icon-nametag:before { 146 | content: '\0068' 147 | } 148 | 149 | .icon-mail:before { 150 | content: '\0075' 151 | } 152 | 153 | .icon-mail-alt:before { 154 | content: '\0079' 155 | } 156 | 157 | .icon-mail-plus:before { 158 | content: '\006B' 159 | } 160 | 161 | .icon-mail-exclaimation:before { 162 | content: '\03BB' 163 | } 164 | 165 | .icon-book:before { 166 | content: '\006F' 167 | } 168 | 169 | .icon-book-alt:before { 170 | content: '\00A4' 171 | } 172 | 173 | .icon-book-open:before { 174 | content: '\00B4' 175 | } 176 | 177 | .icon-files:before { 178 | content: '\03C3' 179 | } 180 | 181 | .icon-popup:before { 182 | content: '\0070' 183 | } 184 | 185 | .icon-bell:before { 186 | content: '\0071' 187 | } 188 | 189 | .icon-menu:before { 190 | content: '\0074' 191 | } 192 | 193 | .icon-home:before { 194 | content: '\0077' 195 | } 196 | 197 | .icon-home-alt:before { 198 | content: '\03DB' 199 | } 200 | 201 | .icon-lightning:before { 202 | content: '\0034' 203 | } 204 | 205 | .icon-blitz:before { 206 | content: '\0034' 207 | } 208 | 209 | .icon-bullet:before { 210 | content: '\0035' 211 | } 212 | 213 | .icon-chess960:before { 214 | content: '\0036' 215 | } 216 | 217 | .icon-chess-board-puzzle:before { 218 | content: '\0037' 219 | } 220 | 221 | .icon-chess-board-puzzle-reversed:before { 222 | content: '\03DE' 223 | } 224 | 225 | .icon-fire-puzzle:before { 226 | content: '\0371' 227 | } 228 | 229 | .icon-computer:before { 230 | content: '\0039' 231 | } 232 | 233 | .icon-computer-search:before { 234 | content: '\03BE' 235 | } 236 | 237 | .icon-tag:before { 238 | content: '\00BD' 239 | } 240 | 241 | .icon-toolbox:before { 242 | content: '\00BC' 243 | } 244 | 245 | .icon-cake:before { 246 | content: '\00BE' 247 | } 248 | 249 | .icon-asterisk:before { 250 | content: '\002A' 251 | } 252 | 253 | .icon-trash:before { 254 | content: '\2022' 255 | } 256 | 257 | .icon-download:before { 258 | content: '\0022' 259 | } 260 | 261 | .icon-display-grid:before { 262 | content: '\00AB' 263 | } 264 | 265 | .icon-display-slider:before { 266 | content: '\2039' 267 | } 268 | 269 | .icon-display-list:before { 270 | content: '\203A' 271 | } 272 | 273 | .icon-pushpin:before { 274 | content: '\00BB' 275 | } 276 | 277 | .icon-key:before { 278 | content: '\00A2' 279 | } 280 | 281 | .icon-page:before { 282 | content: '\00A3' 283 | } 284 | 285 | .icon-page-alt:before { 286 | content: '\20AC' 287 | } 288 | 289 | .icon-page-pencil:before { 290 | content: '\0078' 291 | } 292 | 293 | .icon-news:before { 294 | content: '\0045' 295 | } 296 | 297 | .icon-equal:before { 298 | content: '\003D' 299 | } 300 | 301 | .icon-stats:before { 302 | content: '\003B' 303 | } 304 | 305 | .icon-stats-arrow-up:before { 306 | content: '\010D' 307 | } 308 | 309 | .icon-stats-x:before { 310 | content: '\00F0' 311 | } 312 | 313 | .icon-graphs:before { 314 | content: '\03C5' 315 | } 316 | 317 | .icon-binoculars:before { 318 | content: '\2014' 319 | } 320 | 321 | .icon-magnifying-glass:before { 322 | content: '\2013' 323 | } 324 | 325 | .icon-present:before { 326 | content: '\00BE' 327 | } 328 | 329 | .icon-exit:before { 330 | content: '\00D7' 331 | } 332 | 333 | .icon-handshake:before { 334 | content: '\002B' 335 | } 336 | 337 | .icon-cup:before { 338 | content: '\2021' 339 | } 340 | 341 | .icon-paper-pencil:before { 342 | content: '\03A3' 343 | } 344 | 345 | .icon-todo-list:before { 346 | content: '\010F' 347 | } 348 | 349 | .icon-trophy-plus:before { 350 | content: '\03A5' 351 | } 352 | 353 | .icon-trophy-minus:before { 354 | content: '\03BA' 355 | } 356 | 357 | .icon-trophy-podium:before { 358 | content: '\03B5' 359 | } 360 | 361 | .icon-privacy:before { 362 | content: '\03B2' 363 | } 364 | 365 | .icon-survey:before { 366 | content: '\03C1' 367 | } 368 | 369 | .icon-crossed-swords:before { 370 | content: '\03C4' 371 | } 372 | 373 | .icon-checkmark-box:before { 374 | content: '\03A8' 375 | } 376 | 377 | .icon-checkmark-box-plus:before { 378 | content: '\03A6' 379 | } 380 | 381 | .icon-tracked-content:before { 382 | content: '\00D5' 383 | } 384 | 385 | .icon-queen-wreath:before { 386 | content: '\00F1' 387 | } 388 | 389 | .icon-crosshair:before { 390 | content: '\0111' 391 | } 392 | 393 | .icon-shield:before { 394 | content: '\014B' 395 | } 396 | 397 | .icon-chip:before { 398 | content: '\00F6' 399 | } 400 | 401 | .icon-smiley:before { 402 | content: '\03C2' 403 | } 404 | 405 | .icon-eye:before { 406 | content: '\0057' 407 | } 408 | 409 | .icon-select:before { 410 | content: '\03AD' 411 | } 412 | 413 | .icon-undo:before { 414 | content: '\004C' 415 | } 416 | 417 | .icon-link:before { 418 | content: '\0041' 419 | } 420 | 421 | .icon-x:before { 422 | content: '\0042' 423 | } 424 | 425 | .icon-reply:before { 426 | content: '\0043' 427 | } 428 | 429 | .icon-checkmark:before { 430 | content: '\0047' 431 | } 432 | 433 | .icon-redo:before { 434 | content: '\003A' 435 | } 436 | 437 | .icon-plus:before { 438 | content: '\0056' 439 | } 440 | 441 | .icon-list:before { 442 | content: '\0072' 443 | } 444 | 445 | .icon-embed:before { 446 | content: '\221E' 447 | } 448 | 449 | .icon-image-plus:before { 450 | content: '\03B6' 451 | } 452 | 453 | .icon-follow:before { 454 | content: '\03CE' 455 | } 456 | 457 | .icon-unfollow:before { 458 | content: '\03CD' 459 | } 460 | 461 | .icon-quote:before { 462 | content: '\00D6' 463 | } 464 | 465 | .icon-circle:before { 466 | content: '\0054' 467 | } 468 | 469 | .icon-circle-dashboard:before { 470 | content: '\004E' 471 | } 472 | 473 | .icon-circle-x:before { 474 | content: '\0051' 475 | } 476 | 477 | .icon-circle-3-dots:before { 478 | content: '\038F' 479 | } 480 | 481 | .icon-circle-timer:before { 482 | content: '\0033' 483 | } 484 | 485 | .icon-standard:before { 486 | content: '\0033' 487 | } 488 | 489 | .icon-circle-gearwheel:before { 490 | content: '\00B7' 491 | } 492 | 493 | .icon-circle-clock:before { 494 | content: '\0027' 495 | } 496 | 497 | .icon-circle-clock-alt:before { 498 | content: '\00B0' 499 | } 500 | 501 | .icon-circle-question:before { 502 | content: '\0028' 503 | } 504 | 505 | .icon-circle-info:before { 506 | content: '\0029' 507 | } 508 | 509 | .icon-circle-arrow:before { 510 | content: '\00F7' 511 | } 512 | 513 | .icon-circle-block:before { 514 | content: '\222B' 515 | } 516 | 517 | .icon-circle-stop:before { 518 | content: '\0026' 519 | } 520 | 521 | .icon-circle-danger:before { 522 | content: '\2020' 523 | } 524 | 525 | .icon-circle-checkmark:before { 526 | content: '\03C7' 527 | } 528 | 529 | .icon-square-reply:before { 530 | content: '\0058' 531 | } 532 | 533 | .icon-square-pencil:before { 534 | content: '\005A' 535 | } 536 | 537 | .icon-square-brush:before { 538 | content: '\006C' 539 | } 540 | 541 | .icon-square-in:before { 542 | content: '\00A1' 543 | } 544 | 545 | .icon-square-out:before { 546 | content: '\00BF' 547 | } 548 | 549 | .icon-square-bottom-in:before { 550 | content: '\039E' 551 | } 552 | 553 | .icon-square-x:before { 554 | content: '\00FC' 555 | } 556 | 557 | .icon-square-checkmark:before { 558 | content: '\03BF' 559 | } 560 | 561 | .icon-square-four:before { 562 | content: '\03F8' 563 | } 564 | 565 | .icon-caret-up:before { 566 | content: '\007C' 567 | } 568 | 569 | .icon-caret-down:before { 570 | content: '\003F' 571 | } 572 | 573 | .icon-caret-left:before { 574 | content: '\002F' 575 | } 576 | 577 | .icon-caret-right:before { 578 | content: '\005C' 579 | } 580 | 581 | .icon-chevron-up:before { 582 | content: '\003E' 583 | } 584 | 585 | .icon-chevron-bottom:before { 586 | content: '\003C' 587 | } 588 | 589 | .icon-chevron-left:before { 590 | content: '\002C' 591 | } 592 | 593 | .icon-chevron-right:before { 594 | content: '\2026' 595 | } 596 | 597 | .icon-double-chevron-left:before { 598 | content: '\0021' 599 | } 600 | 601 | .icon-double-chevron-right:before { 602 | content: '\03B1' 603 | } 604 | 605 | .icon-chevron-previous:before { 606 | content: '\0023' 607 | } 608 | 609 | .icon-chevron-next:before { 610 | content: '\0040' 611 | } 612 | 613 | .icon-chevron-down:before { 614 | content: '\0030' 615 | } 616 | 617 | .icon-arrow-return:before { 618 | content: '\005F' 619 | } 620 | 621 | .icon-arrow-up:before { 622 | content: '\007D' 623 | } 624 | 625 | .icon-arrow-down:before { 626 | content: '\007B' 627 | } 628 | 629 | .icon-arrow-left:before { 630 | content: '\005B' 631 | } 632 | 633 | .icon-arrow-right:before { 634 | content: '\005D' 635 | } 636 | 637 | .icon-round-arrow-return:before { 638 | content: '\03BC' 639 | } 640 | 641 | .icon-bold-arrow-right:before { 642 | content: '\0386' 643 | } 644 | 645 | .icon-arrow-cross:before { 646 | content: '\0110' 647 | } 648 | 649 | .icon-user:before { 650 | content: '\0062' 651 | } 652 | 653 | .icon-users:before { 654 | content: '\006D' 655 | } 656 | 657 | .icon-users-alt:before { 658 | content: '\006E' 659 | } 660 | 661 | .icon-user-info:before { 662 | content: '\0076' 663 | } 664 | 665 | .icon-user-question:before { 666 | content: '\002D' 667 | } 668 | 669 | .icon-user-search:before { 670 | content: '\0024' 671 | } 672 | 673 | .icon-user-search-alt:before { 674 | content: '\014a' 675 | } 676 | 677 | .icon-user-feed:before { 678 | content: '\0025' 679 | } 680 | 681 | .icon-user-block:before { 682 | content: '\00A6' 683 | } 684 | 685 | .icon-user-plus:before { 686 | content: '\00B6' 687 | } 688 | 689 | .icon-users-plus:before { 690 | content: '\00FF' 691 | } 692 | 693 | .icon-user-x:before { 694 | content: '\00A7' 695 | } 696 | 697 | .icon-user-chain:before { 698 | content: '\0159' 699 | } 700 | 701 | .icon-user-broken-chain:before { 702 | content: '\0158' 703 | } 704 | 705 | .icon-user-shield:before { 706 | content: '\03DA' 707 | } 708 | 709 | .icon-user-shield-plus:before { 710 | content: '\0373' 711 | } 712 | 713 | .icon-chess-board:before { 714 | content: '\0069' 715 | } 716 | 717 | .icon-chess-board-alt:before { 718 | content: '\2019' 719 | } 720 | 721 | .icon-chess-board-search:before { 722 | content: '\0394' 723 | } 724 | 725 | .icon-chess-board-search-alt:before { 726 | content: '\03A9' 727 | } 728 | 729 | .icon-question:before { 730 | content: '\00A9' 731 | } 732 | 733 | .icon-chess-board-plus:before { 734 | content: '\02C6' 735 | } 736 | 737 | .icon-chess-board-arrow:before { 738 | content: '\0038' 739 | } 740 | 741 | .icon-chess-board-circle:before { 742 | content: '\00AB' 743 | } 744 | 745 | .icon-chess-board-gear:before { 746 | content: '\03F7' 747 | } 748 | 749 | .icon-chess-crown:before { 750 | content: '\03FB' 751 | } 752 | 753 | .icon-chess-pawn:before { 754 | content: '\0031' 755 | } 756 | 757 | .icon-chess-pawn-left-half-rook:before { 758 | content: '\0112' 759 | } 760 | 761 | .icon-chess-pawn-right-half-rook:before { 762 | content: '\0113' 763 | } 764 | 765 | .icon-chess-pawn-rook:before { 766 | content: '\0073' 767 | } 768 | 769 | .icon-chess-pawn-square:before { 770 | content: '\03B7' 771 | } 772 | 773 | .icon-chess-pawns:before { 774 | content: '\0073' 775 | } 776 | 777 | .icon-chess-move:before { 778 | content: '\0030' 779 | } 780 | 781 | .icon-chess-move-alt:before { 782 | content: '\006A' 783 | } 784 | 785 | .icon-chess-board-folder:before { 786 | content: '\0398' 787 | } 788 | 789 | .icon-chess-board-paper:before { 790 | content: '\03A5' 791 | } 792 | 793 | .icon-chess-board-arrow-down:before { 794 | content: '\03CC' 795 | } 796 | 797 | .icon-chess-board-arrow-right:before { 798 | content: '\03CC' 799 | } 800 | 801 | .icon-chess-board-arrow-right { 802 | position: relative; 803 | top: 2px; 804 | left: -2px; 805 | transform: rotate(-90deg); 806 | } 807 | 808 | .icon-chess-board-arrow-left:before { 809 | content: '\03CC' 810 | } 811 | 812 | .icon-chess-board-arrow-left { 813 | position: relative; 814 | top: 2px; 815 | left: 2px; 816 | transform: rotate(90deg); 817 | } 818 | 819 | .icon-checkbox:before { 820 | content: '\03A8' 821 | } 822 | 823 | .icon-checkbox-plus:before { 824 | content: '\03A6' 825 | } 826 | 827 | .icon-printer:before { 828 | content: '\00E5' 829 | } 830 | 831 | .icon-play:before { 832 | content: '\004A' 833 | } 834 | 835 | .icon-pause:before { 836 | content: '\004B' 837 | } 838 | 839 | .icon-sound-off:before { 840 | content: '\0050' 841 | } 842 | 843 | .icon-sound-on:before { 844 | content: '\0055' 845 | } 846 | 847 | .icon-repeat:before { 848 | content: '\0066' 849 | } 850 | 851 | .icon-shuffle:before { 852 | content: '\0049' 853 | } 854 | 855 | .icon-resize:before { 856 | content: '\03C0' 857 | } 858 | 859 | .icon-favorites:before { 860 | content: '\03AE' 861 | } 862 | 863 | .icon-facebook:before { 864 | content: '\0053' 865 | } 866 | 867 | .icon-facebook-alt:before { 868 | content: '\0044' 869 | } 870 | 871 | .icon-twitter:before { 872 | content: '\0046' 873 | } 874 | 875 | .icon-linkedin:before { 876 | content: '\00C5' 877 | } 878 | 879 | .icon-tumblr:before { 880 | content: '\00EB' 881 | } 882 | 883 | .icon-stumbleupon:before { 884 | content: '\00DF' 885 | } 886 | 887 | .icon-reddit:before { 888 | content: '\00B1' 889 | } 890 | 891 | .icon-google-plus:before { 892 | content: '\00AE' 893 | } 894 | 895 | .icon-youtube:before { 896 | content: '\0060' 897 | } 898 | 899 | .icon-share:before { 900 | content: '\00A5' 901 | } 902 | 903 | .icon-twitch:before { 904 | content: '\0393' 905 | } 906 | 907 | .icon-android:before { 908 | content: '\00FE' 909 | } 910 | 911 | .icon-apple:before { 912 | content: '\25CA' 913 | } 914 | 915 | .icon-win-phone:before { 916 | content: '\03A0' 917 | } 918 | 919 | .icon-thumbs-down:before { 920 | content: '\2265' 921 | } 922 | 923 | .icon-thumbs-up:before { 924 | content: '\2264' 925 | } 926 | 927 | .icon-card:before { 928 | content: '\03DC' 929 | } 930 | 931 | .icon-paypal:before { 932 | content: '\0377' 933 | } 934 | 935 | .icon-membership-diamond:before { 936 | content: '\0370' 937 | } 938 | 939 | .icon-membership-platinum:before { 940 | content: '\0376' 941 | } 942 | 943 | .icon-membership-staff:before { 944 | content: '\0372' 945 | } 946 | 947 | .icon-membership-mod:before { 948 | content: '\0372' 949 | } 950 | 951 | .icon-membership-gold:before { 952 | content: '\03AE' 953 | } 954 | 955 | .icon-circle-hollow:before { 956 | content: '\03BD' 957 | } 958 | 959 | .icon-binoculars-crossed:before { 960 | content: '\00DE' 961 | } 962 | 963 | .icon-border-resize:before { 964 | content: '\03AC' 965 | } 966 | 967 | .icon-maximize:before { 968 | content: '\03DD' 969 | } 970 | 971 | .icon-minimize:before { 972 | content: '\03D8' 973 | } 974 | 975 | .icon-live360:before { 976 | content: '\00D1' 977 | } 978 | 979 | .icon-signal:before { 980 | content: '\03AF' 981 | } 982 | 983 | .icon-captured-bishop:before { 984 | content: '\0062' 985 | } 986 | 987 | .icon-captured-bishop-2:before { 988 | content: '\0042' 989 | } 990 | 991 | .icon-captured-knight:before { 992 | content: '\006E' 993 | } 994 | 995 | .icon-captured-knight-2:before { 996 | content: '\004E' 997 | } 998 | 999 | .icon-captured-rook:before { 1000 | content: '\0072' 1001 | } 1002 | 1003 | .icon-captured-rook-2:before { 1004 | content: '\0052' 1005 | } 1006 | 1007 | .icon-captured-king:before { 1008 | content: '\006B' 1009 | } 1010 | 1011 | .icon-captured-queen:before { 1012 | content: '\0071' 1013 | } 1014 | 1015 | .icon-captured-pawn:before { 1016 | content: '\0031' 1017 | } 1018 | 1019 | .icon-captured-pawn-2:before { 1020 | content: '\0032' 1021 | } 1022 | 1023 | .icon-captured-pawn-3:before { 1024 | content: '\0033' 1025 | } 1026 | 1027 | .icon-captured-pawn-4:before { 1028 | content: '\0034' 1029 | } 1030 | 1031 | .icon-captured-pawn-5:before { 1032 | content: '\0035' 1033 | } 1034 | 1035 | .icon-captured-pawn-6:before { 1036 | content: '\0036' 1037 | } 1038 | 1039 | .icon-captured-pawn-7:before { 1040 | content: '\0037' 1041 | } 1042 | 1043 | .icon-captured-pawn-8:before { 1044 | content: '\0038' 1045 | } 1046 | -------------------------------------------------------------------------------- /app/components/Icon.js: -------------------------------------------------------------------------------- 1 | /* eslint quote-props: 0 */ 2 | import React, { PropTypes } from 'react'; 3 | import classNames from 'classnames/bind'; 4 | import icon from './Icon.css'; 5 | import BaseComponent from '../BaseComponent'; 6 | 7 | const cx = classNames.bind(icon); 8 | 9 | export default class Icon extends BaseComponent { 10 | 11 | static propTypes = { 12 | name: PropTypes.string.isRequired, 13 | size: PropTypes.string, 14 | color: PropTypes.string, 15 | className: PropTypes.any 16 | }; 17 | 18 | render() { 19 | const iconName = `icon-${this.props.name}`; 20 | const iconClassName = icon[iconName]; 21 | 22 | const iconClass = cx({ 23 | 'icon': true, 24 | [iconClassName]: true, 25 | [this.props.className]: true 26 | }); 27 | 28 | const iconStyle = { 29 | fontSize: parseInt(this.props.size, 10), 30 | color: `rgba(${this.props.color})` 31 | }; 32 | 33 | return ( 34 | 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/components/Link.css: -------------------------------------------------------------------------------- 1 | .link:hover { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /app/components/Link.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import style from './Link.css'; 3 | import BaseComponent from '../BaseComponent'; 4 | 5 | export default class Link extends BaseComponent { 6 | 7 | static propTypes = { 8 | slug: PropTypes.string.isRequired, 9 | children: PropTypes.element.isRequired 10 | }; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.goTo = this.goTo.bind(this); 15 | } 16 | 17 | // We need to a routing function to handle actions 18 | // that send the user to various url targets on site 19 | goTo() { 20 | chrome.tabs.create({ url: `https://www.chess.com/${this.props.slug}` }); 21 | window.close(); 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 | { this.props.children } 28 |
29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/components/NotificationBar.css: -------------------------------------------------------------------------------- 1 | .notificationBar { 2 | padding: 10px 0px 10px 0px; 3 | } 4 | .notificationBar div { 5 | position:relative; 6 | display:inline-block; 7 | margin-left: 20%; 8 | } 9 | .notificationBar div span span { 10 | position: absolute; 11 | right:-20px; 12 | top:10px; 13 | background: #b43430; 14 | color: white; 15 | text-align: center; 16 | padding: 0px 5px 0px 5px; 17 | font-size:12px; 18 | font-weight: bold; 19 | border-radius: 30px 30px 30px 30px; 20 | } -------------------------------------------------------------------------------- /app/components/NotificationBar.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import style from './NotificationBar.css'; 3 | import Icon from './Icon.js'; 4 | import Link from './Link.js'; 5 | import BaseComponent from '../BaseComponent'; 6 | 7 | export default class NotificationBar extends BaseComponent { 8 | 9 | static propTypes = { 10 | notifications: PropTypes.object.isRequired, 11 | }; 12 | 13 | render() { 14 | let games = (); 15 | let messages = (); 16 | let alerts = (); 17 | 18 | if (!this.props.notifications.loading) { 19 | games = ({this.props.notifications.games}); 20 | messages = ({this.props.notifications.messages}); 21 | alerts = ({this.props.notifications.alerts}); 22 | } 23 | 24 | return ( 25 |
26 | 27 | 28 | 29 | {games} 30 | 31 | 32 | 33 | 34 | 35 | {messages} 36 | 37 | 38 | 39 | 40 | 41 | {alerts} 42 | 43 | 44 |
45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/components/Options/Config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | groups: [ 3 | { 4 | id: 'backgrounds', 5 | title: 'Backgrounds', 6 | icon: 'page-alt', 7 | options: [ 8 | { 9 | type: 'ColorPicker', 10 | name: 'content', 11 | title: 'Content Background Color', 12 | property: 'backgroundColor', 13 | selector: [ 14 | '.index #content > section', 15 | '.user-home #content section', 16 | '#content > section, #content .forum-category-header', 17 | '#content .forum-table-full tbody>tr:hover', 18 | '#content .table .row-highlighted>td', 19 | '#content .table.with-row-highlight>tbody>tr:hover', 20 | '#content .switchers a.iconized.active' 21 | ] 22 | }, 23 | { 24 | type: 'ColorPicker', 25 | name: 'header-sections', 26 | title: 'Header Sections Color', 27 | property: 'backgroundColor', 28 | selector: [ 29 | 'main #sidebar .section-header', 30 | '#content #load-more-container', 31 | '#sidebar .section-header', 32 | '.user-home #sidebar .section-header', 33 | '#content .section-header' 34 | ] 35 | }, 36 | { 37 | type: 'ColorPicker', 38 | name: 'sidebar', 39 | title: 'Sidebar Background Color', 40 | property: 'backgroundColor', 41 | selector: [ 42 | '#sidebar section:not(#chess-board-sidebar)', 43 | '#sidebar section:not(#chess-board-sidebar) .section-wrapper', 44 | 'ul.stats-list li', 45 | '.daily-chess #sidebar section:not(#chess-board-sidebar)' 46 | ] 47 | } 48 | ] 49 | }, 50 | { 51 | id: 'text', 52 | title: 'Text', 53 | icon: 'paper-pencil', 54 | options: [ 55 | { 56 | type: 'ColorPicker', 57 | name: 'text', 58 | title: 'Text Color', 59 | property: 'color', 60 | selector: [ 61 | '#content section.section-wrapper:not(.dismissible-banner):not(.intro)', 62 | '#content article', 63 | '#content .content-body', 64 | '#content .content-body h1', 65 | '#content h1, .index p', 66 | '.view-thread .comments li .user-content > p', 67 | '#comments .comments .comment-body' 68 | ] 69 | }, 70 | { 71 | type: 'ColorPicker', 72 | name: 'sidebar-text', 73 | title: 'Sidebar Text Color', 74 | property: 'color', 75 | selector: [ 76 | 'sidebar .member-item [class^=\'icon-\']', 77 | '#sidebar .members-stats .member-item .number', 78 | '#sidebar .members-stats .member-item .stat, #sidebar .place-number', 79 | '#sidebar section:not(#chess-board-sidebar) .user-rating', 80 | '#sidebar .survey-container .survey-item label', 81 | '#sidebar section:not(#chess-board-sidebar) a', 82 | '#sidebar section .section-clickable h3', 83 | '.white-header h3, #sidebar ul.rating-list', 84 | '#sidebar ul.stats-list li', 85 | '#sidebar ul.stats-list aside', 86 | '#sidebar ul.stats-list aside span', 87 | '#sidebar .new-game-time-header' 88 | ] 89 | }, 90 | { 91 | type: 'ColorPicker', 92 | name: 'link', 93 | title: 'Link Color', 94 | property: 'color', 95 | selector: [ 96 | '.index article.content a', 97 | '.user-home #content .section-wrapper a', 98 | '#content section a:not(.btn)', 99 | '#content a.username', 100 | '#content span.username' 101 | ] 102 | }, 103 | { 104 | type: 'ColorPicker', 105 | name: 'header-text', 106 | title: 'Section Header Text Color', 107 | property: 'color', 108 | selector: [ 109 | 'content #load-more-container a', 110 | '#content #load-more-container span', 111 | '#content .header-clickable h3.section-clickable a', 112 | '#content .section-header h3', 113 | '#content .header-clickable h3', 114 | '#content .header-clickable h3 a', 115 | '#sidebar .section-header h3', 116 | '#sidebar .header-clickable h3.section-clickable a', 117 | '.section-header a', 118 | 'main #content.content-container .section-header .header-count', 119 | 'main #sidebar .section-header .header-count', 120 | '.view-thread > h1' 121 | ] 122 | }, 123 | { 124 | type: 'ColorPicker', 125 | name: 'meta-color', 126 | title: 'Meta Information Color', 127 | property: 'color', 128 | selector: [ 129 | 'content ul.content-stats', 130 | '#content ul.content-stats>li [class^=icon-]', 131 | '.content-stats time, #content .user-chess-title', 132 | '.user-home ul.content-stats>li [class^=icon-]', 133 | '.user-home .content-stats time, .user-home .content-stats .user-chess-title', 134 | '.user-home ul.content-stats', 135 | '.user-home .content-container ul.content-list>.list-short .amount', 136 | '.user-home ul.content-list>li', 137 | '.forum-thread time', 138 | '#content ul.forum-thread>li .cell-time-data>a i', 139 | '.view-thread .comments .comment-header time', 140 | '.view-thread .pagination ul li', 141 | '.view-thread .pagination [class^=\'icon-\']', 142 | '.navigation .pagination ul li', 143 | '.view-thread .social-and-follow label', 144 | '#content .club-info', 145 | '#content .more-from h2', 146 | '#comments .comments .comment-header time', 147 | '#content .user-rating' 148 | ] 149 | }, 150 | { 151 | type: 'ColorPicker', 152 | name: 'icon-color', 153 | title: 'Icon Color', 154 | property: 'color', 155 | selector: [ 156 | '.section-header [class^=\'icon-\']', 157 | '.section-header [class*=\' icon-\']', 158 | 'main .header-clickable h3.section-clickable::before', 159 | '#sidebar .section-clickable [class^=\'icon-\']', 160 | '#sidebar section .iconized>i', 161 | '.new-game-time [class^=\'icon-\']', 162 | '#content section .iconized>i', 163 | '#content section.section-header .iconized>i', 164 | '#content section.section-row .iconized>i', 165 | '#content .section-wrapper [class^=\'icon-\']' 166 | ] 167 | }, 168 | { 169 | type: 'FontFamily', 170 | optionFonts: [ 171 | { 172 | title: 'Default', 173 | value: '' 174 | }, 175 | { 176 | title: 'Trebuchet (V2)', 177 | value: 'Trebuchet MS' 178 | }, 179 | { 180 | title: 'System Default', 181 | value: 'system,-apple-system,BlinkMacSystemFont,Segoe UI,' + 182 | 'Helvetica Neue,Helvetica,Lucida Grande' 183 | }, 184 | { 185 | title: 'Helvetica', 186 | value: 'Helvetica' 187 | }, 188 | { 189 | title: 'Arial', 190 | value: 'Arial' 191 | } 192 | ] 193 | } 194 | ] 195 | }, 196 | { 197 | id: 'buttons', 198 | title: 'Buttons', 199 | icon: 'square-checkmark', 200 | options: [ 201 | { 202 | type: 'ColorPicker', 203 | name: 'button-primary-color', 204 | title: 'Button Primary Color', 205 | property: 'backgroundColor', 206 | selector: [ 207 | '#sidebar .btn.btn-primary' 208 | ] 209 | }, 210 | { 211 | type: 'ColorPicker', 212 | name: 'button-primary-text-color', 213 | title: 'Button Primary Text Color', 214 | property: 'color', 215 | selector: [ 216 | '#sidebar .btn.btn-primary' 217 | ] 218 | }, 219 | { 220 | type: 'ColorPicker', 221 | name: 'button-secondary-color', 222 | title: 'Button Secondary Color', 223 | property: 'backgroundColor', 224 | selector: [ 225 | 'sidebar .btn.btn-arrow', 226 | '.game-controls .btn', 227 | '.quick-game-controls-row .btn', 228 | '.new-game-time .btn', 229 | '.view-thread .comments .comment-number' 230 | ] 231 | }, 232 | { 233 | type: 'ColorPicker', 234 | name: 'button-secondary-text-color', 235 | title: 'Button Secondary Text Color', 236 | property: 'color', 237 | selector: [ 238 | 'sidebar .btn.btn-arrow', 239 | '#sidebar .btn.btn-arrow::after', 240 | '#sidebar .btn.btn-arrow .format-icon', 241 | '#sidebar .game-controls .control-group .btn.btn-icon i', 242 | '#sidebar .quick-game-controls-row .btn', 243 | '.new-game-time .btn' 244 | ] 245 | } 246 | ] 247 | }, 248 | { 249 | id: 'hide', 250 | title: 'Hide', 251 | icon: 'circle-stop', 252 | options: [ 253 | { 254 | type: 'ToggleDisplay', 255 | name: 'activity', 256 | title: 'Hide Activity', 257 | selector: [ 258 | '.user-home #content > div > recent-content > section', //https://www.chess.com/home 259 | '.social-share-present #content > div:nth-child(7) > recent-content > section', //https://www.chess.com/member/wistcc 260 | '.social-share-present #content > div:nth-child(7)' //https://www.chess.com/member/wistcc?view=trophies 261 | ] 262 | }, 263 | { 264 | type: 'ToggleDisplay', 265 | name: 'new-game', 266 | title: 'Hide New Game', 267 | selector: [ 268 | '#sidebar > section.new-game-container.with-friends-list.recent-opponents', //https://www.chess.com/daily 269 | '.user-home #sidebar > section.new-game-container.recent-opponents' //https://www.chess.com/home 270 | ] 271 | }, 272 | { 273 | type: 'ToggleDisplay', 274 | name: 'friends', 275 | title: 'Hide Friends', 276 | selector: [ 277 | '.social-share-present #sidebar > section:nth-child(2)', //https://www.chess.com/member/wistcc 278 | '#sidebar > section.new-game-container.with-friends-list.recent-opponents > div.anim-panel.shown > ul.section-wrapper.users-grid', //https://www.chess.com/daily 279 | '.user-home #sidebar > section:nth-child(4)', //https://www.chess.com/home 280 | '#sidebar > section:nth-child(2)', //https://www.chess.com/members 281 | ], 282 | helpers: [ 283 | { 284 | type: 'hide', 285 | selector: '.user-home #sidebar section:nth-child(3)', 286 | relation: 'clubs' 287 | } 288 | ] 289 | }, 290 | { 291 | type: 'ToggleDisplay', 292 | name: 'clubs', 293 | title: 'Hide Clubs', 294 | selector: [ 295 | '.clubs-index #sidebar > section:nth-child(1)' //https://www.chess.com/clubs 296 | ], 297 | helpers: [ 298 | { 299 | type: 'hide', 300 | selector: '.user-home #sidebar section:nth-child(3)', 301 | relation: 'friends' 302 | } 303 | ] 304 | }, 305 | { 306 | type: 'ToggleDisplay', 307 | name: 'stats', 308 | title: 'Hide Stats', 309 | selector: [ 310 | '.user-home #user-rating-sidebar', //https://www.chess.com/home 311 | '.stats #sidebar > section:nth-child(1)', //https://www.chess.com/stats/daily?type=chess 312 | '.social-share-present #user-rating-sidebar', //https://www.chess.com/member/wistcc 313 | '#sidebar > section.rating-sidebar' //https://www.chess.com/daily 314 | ] 315 | }, 316 | { 317 | type: 'ToggleDisplay', 318 | name: 'trophies', 319 | title: 'Hide Trophies', 320 | selector: [ 321 | '.social-share-present #sidebar-user-trophy-showcase', //https://www.chess.com/member/wistcc 322 | '.user-home #sidebar-user-trophy-showcase', //https://www.chess.com/home 323 | ] 324 | } 325 | ] 326 | } 327 | ] 328 | }; 329 | -------------------------------------------------------------------------------- /app/components/Options/Options.css: -------------------------------------------------------------------------------- 1 | .options { 2 | background-color: #fff; 3 | } 4 | 5 | .options li { 6 | list-style-type: none; 7 | padding: 0; 8 | } 9 | 10 | .options > li { 11 | border-bottom: 1px solid #e8e7e6; 12 | } 13 | 14 | .options ul li { 15 | padding: 0 15px; 16 | } 17 | 18 | .sectionHeading { 19 | color: rgb(43, 43, 43); 20 | font-size: 14px; 21 | font-weight: 600; 22 | line-height: 20px; 23 | display: block; 24 | padding: 1rem; 25 | -webkit-user-select: none; 26 | } 27 | 28 | .sectionHeading:hover { 29 | cursor: pointer; 30 | } 31 | 32 | .sectionHeadingIcon { 33 | float: left; 34 | margin-right: 10px; 35 | position: relative; 36 | top: -3px; 37 | } 38 | 39 | .toggleIcon { 40 | float: right; 41 | } 42 | 43 | .active, .inactive { 44 | transition-duration: 0.3s; 45 | transition-property: transform; 46 | } 47 | 48 | .active { 49 | transform: rotate(90deg); 50 | } 51 | 52 | .inactive { 53 | transform: rotate(0deg); 54 | } 55 | 56 | select { 57 | -webkit-appearance: none; 58 | -moz-appearance: none; 59 | text-indent: 1px; 60 | padding: 5px; 61 | display: inline-block; 62 | cursor: pointer; 63 | position: relative; 64 | width: 50%; 65 | float: right; 66 | background: no-repeat #ffffff; 67 | background-position: right; 68 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABkAAAAeCAYAAADZ7LXbAAAACXBIWXMAAAsTAAALEwEAmpwYAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4YSRudE8o9VGjUYPjGnGXOMk423GbcajJgYmISZLTepN7ppSTbmmKaY7TDtMx83MzaLN1pk1mz0x1zLnm+eb15vft2BaeFostqi2uGVJsuRaplnutrxuhVo5WaVYVVpds0atna0l1rutu6cRp7lOk06rntZnw7Dxtsm2qbcZsOXYBtuutm22fWFnYhdnt8Wuw+6TvZN9un2N/T0HDYfZDqsdWh1+c7RyFDpWOt6azpzuP33F9JbpL2dYzxDP2DPjthPLKcRpnVOb00dnF2e5c4PziIuJS4LLLpc+Lpsbxt3IveRKdPVxXeF60vWdm7Obwu2o26/uNu5p7ofcn8w0nymeWTNz0MPIQ+BR5dE/C5+VMGvfrH5PQ0+BZ7XnIy9jL5FXrdewt6V3qvdh7xc+9j5yn+M+4zw33jLeWV/MN8C3yLfLT8Nvnl+F30N/I/9k/3r/0QCngCUBZwOJgUGBWwL7+Hp8Ib+OPzrbZfay2e1BjKC5QRVBj4KtguXBrSFoyOyQrSH355jOkc5pDoVQfujW0Adh5mGLw34MJ4WHhVeGP45wiFga0TGXNXfR3ENz30T6RJZE3ptnMU85ry1KNSo+qi5qPNo3ujS6P8YuZlnM1VidWElsSxw5LiquNm5svt/87fOH4p3iC+N7F5gvyF1weaHOwvSFpxapLhIsOpZATIhOOJTwQRAqqBaMJfITdyWOCnnCHcJnIi/RNtGI2ENcKh5O8kgqTXqS7JG8NXkkxTOlLOW5hCepkLxMDUzdmzqeFpp2IG0yPTq9MYOSkZBxQqohTZO2Z+pn5mZ2y6xlhbL+xW6Lty8elQfJa7OQrAVZLQq2QqboVFoo1yoHsmdlV2a/zYnKOZarnivN7cyzytuQN5zvn//tEsIS4ZK2pYZLVy0dWOa9rGo5sjxxedsK4xUFK4ZWBqw8uIq2Km3VT6vtV5eufr0mek1rgV7ByoLBtQFr6wtVCuWFfevc1+1dT1gvWd+1YfqGnRs+FYmKrhTbF5cVf9go3HjlG4dvyr+Z3JS0qavEuWTPZtJm6ebeLZ5bDpaql+aXDm4N2dq0Dd9WtO319kXbL5fNKNu7g7ZDuaO/PLi8ZafJzs07P1SkVPRU+lQ27tLdtWHX+G7R7ht7vPY07NXbW7z3/T7JvttVAVVN1WbVZftJ+7P3P66Jqun4lvttXa1ObXHtxwPSA/0HIw6217nU1R3SPVRSj9Yr60cOxx++/p3vdy0NNg1VjZzG4iNwRHnk6fcJ3/ceDTradox7rOEH0x92HWcdL2pCmvKaRptTmvtbYlu6T8w+0dbq3nr8R9sfD5w0PFl5SvNUyWna6YLTk2fyz4ydlZ19fi753GDborZ752PO32oPb++6EHTh0kX/i+c7vDvOXPK4dPKy2+UTV7hXmq86X23qdOo8/pPTT8e7nLuarrlca7nuer21e2b36RueN87d9L158Rb/1tWeOT3dvfN6b/fF9/XfFt1+cif9zsu72Xcn7q28T7xf9EDtQdlD3YfVP1v+3Njv3H9qwHeg89HcR/cGhYPP/pH1jw9DBY+Zj8uGDYbrnjg+OTniP3L96fynQ89kzyaeF/6i/suuFxYvfvjV69fO0ZjRoZfyl5O/bXyl/erA6xmv28bCxh6+yXgzMV70VvvtwXfcdx3vo98PT+R8IH8o/2j5sfVT0Kf7kxmTk/8EA5jz/GMzLdsAAAAgY0hSTQAAeiUAAICDAAD5/wAAgOkAAHUwAADqYAAAOpgAABdvkl/FRgAAAKRJREFUeNrs1TEKwkAQheEvIoI2nsk7qFdIq1hoJ3gCC5sUVpY23sDKXnvrYOUBbGITG0kQjQriPlgYhmF/3ryFjbIs82nVfEEBEiAB8k+Q+q1IkqSDNVq4lMy3scIkjuP0FSdbjNHMLys6OwyQVlnXEsOS2QP6OL8jkzlmd70jus86eBT8FIu8PqGXg6oFX6ARGthgX+V1ReFnDJAACZAfhFwHAJI7HF2lZGQaAAAAAElFTkSuQmCC); 69 | } -------------------------------------------------------------------------------- /app/components/Options/Options.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Config from './Config'; 3 | import Icon from '../Icon'; 4 | import ColorPicker from '../ColorPicker'; 5 | import ToggleDisplay from '../ToggleDisplay'; 6 | import style from './Options.css'; 7 | import BaseComponent from '../../BaseComponent.js'; 8 | 9 | export default class Options extends BaseComponent { 10 | 11 | constructor() { 12 | super(); 13 | this.state = { fontFamily: '', visible: {} }; 14 | 15 | Config.groups.forEach(group => { 16 | this.state.visible[group.id] = false; 17 | }); 18 | 19 | chrome.storage.local.get(result => { 20 | this.setState({ fontFamily: result.fontFamily, 21 | visible: this.state.visible 22 | }); 23 | }); 24 | 25 | // Reset dropdown list when reset button is hit 26 | chrome.storage.onChanged.addListener(changes => { 27 | try { 28 | const newValue = changes.fontFamily.newValue; 29 | 30 | if (Object.keys(newValue).length === 0) { 31 | this.setState({ 32 | fontFamily: changes.fontFamily.newValue, 33 | visible: this.state.visible 34 | }); 35 | } 36 | } catch (e) { 37 | // empty 38 | } 39 | }); 40 | } 41 | 42 | handleToggle = (e) => { 43 | const id = e.target.id; 44 | this.setState({ fontFamily: this.state.fontFamily, 45 | visible: { 46 | [id]: !this.state.visible[id] 47 | } 48 | }); 49 | } 50 | 51 | createFontOptions = (fonts) => 52 | fonts.map((font, i) => 53 | ) 54 | 55 | handleOnChange = (e) => { 56 | const font = e.target.value; 57 | chrome.storage.local.set({ fontFamily: font }); 58 | this.sendMessageToDOM(font); 59 | this.setState({ fontFamily: font, 60 | visible: this.state.visible 61 | }); 62 | } 63 | 64 | sendMessageToDOM = (font) => { 65 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 66 | chrome.tabs.sendMessage(tabs[0].id, { 67 | update: 'fontFamily', 68 | font 69 | }); 70 | }); 71 | } 72 | 73 | render() { 74 | const groups = Config.groups; 75 | 76 | return ( 77 |
    78 | {groups.map((group) => 79 |
  • 80 |
    85 |
    86 | 91 |
    92 | {group.title} 93 |
    94 | 100 |
    101 |
    102 | {this.state.visible[group.id] ? 103 |
      104 | {group.options.map((option, i) => 105 |
    • 106 | {option.type === 'ColorPicker' ? 107 | 113 | : null} 114 | {option.type === 'FontFamily' ? 115 | (
      116 | 117 | 124 |
      ) 125 | : null} 126 | {option.type === 'ToggleDisplay' ? 127 | 133 | : null} 134 |
    • 135 | )} 136 |
    137 | : null} 138 |
  • 139 | )} 140 |
141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /app/components/Play.css: -------------------------------------------------------------------------------- 1 | .play { 2 | background: #f1f1f1; 3 | padding: 15px; 4 | } 5 | 6 | .choices { 7 | padding: 0 0 10px; 8 | overflow: hidden; 9 | } 10 | 11 | .choicePallete { 12 | padding: 0px 0 15px; 13 | } 14 | 15 | .timeHeader { 16 | margin: 10px 0 5px; 17 | } 18 | 19 | .btn { 20 | background: #e4902d; 21 | display: block; 22 | text-align: center; 23 | color: white; 24 | border-radius: 0.2rem; 25 | padding: 1rem 0; 26 | font-size: 1.1rem; 27 | box-shadow: 0rem 0.07rem 0rem rgba(172,109,35,1); 28 | } 29 | .btn:hover { 30 | cursor: pointer; 31 | background: #d37e19; 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/components/Play.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Variants from './Variants'; 3 | import Times from './Times'; 4 | import Button from './Button'; 5 | import Link from './Link'; 6 | import Icon from '../components/Icon'; 7 | import style from '../components/Play.css'; 8 | import buttonStyle from '../components/Button.css'; 9 | import BaseComponent from '../BaseComponent'; 10 | 11 | export default class Play extends BaseComponent { 12 | 13 | static propTypes = { 14 | api: PropTypes.object.isRequired 15 | }; 16 | 17 | render() { 18 | return ( 19 |
20 |
21 | 31 | 37 |
38 | {this.props.api.showVariantsBox ? 39 | 45 | : null} 46 | {this.props.api.showTimesBox ? 47 | 53 | : null} 54 | Play 55 |
56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/components/PlayContainer.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import lodash from 'lodash'; 3 | import Play from './Play'; 4 | import BaseComponent from '../BaseComponent'; 5 | 6 | export default class PlayContainer extends BaseComponent { 7 | 8 | static propTypes = { 9 | user: PropTypes.object.isRequired 10 | }; 11 | 12 | constructor(props) { 13 | super(props); 14 | this.state = { 15 | variants: [ 16 | { 17 | label: 'Standard', 18 | value: 'chess', 19 | icon: 'chess-board', 20 | daily: true, 21 | live: true 22 | }, 23 | { 24 | label: 'Chess960', 25 | value: 'chess960', 26 | icon: '960', 27 | daily: true, 28 | live: true 29 | }, 30 | { 31 | label: '3 Check', 32 | value: 'threecheck', 33 | icon: 'threecheck', 34 | live: true 35 | }, 36 | { 37 | label: 'King of the Hill', 38 | value: 'kingofthehill', 39 | icon: 'kingofthehill', 40 | live: true 41 | }, 42 | { 43 | label: 'Crazyhouse', 44 | value: 'crazyhouse', 45 | icon: 'crazyhouse', 46 | live: true 47 | } 48 | ], 49 | dailyTimes: [ 50 | { label: '1 day', days: 1 }, 51 | { label: '2 days', days: 2 }, 52 | { label: '3 days', days: 3 }, 53 | { label: '5 days', days: 5 }, 54 | { label: '7 days', days: 7 }, 55 | { label: '10 days', days: 10 }, 56 | { label: '14 days', days: 14 }, 57 | ], 58 | liveTimes: [ 59 | { label: '1 min', minutes: 1, seconds: 0 }, 60 | { label: '2 | 1', minutes: 2, seconds: 1 }, 61 | { label: '3 min', minutes: 3, seconds: 0 }, 62 | { label: '3 | 2', minutes: 3, seconds: 2 }, 63 | { label: '5 min', minutes: 5, seconds: 0 }, 64 | { label: '5 | 5', minutes: 5, seconds: 5 }, 65 | { label: '10 min', minutes: 10, seconds: 0 }, 66 | { label: '15 | 10', minutes: 15, seconds: 10 }, 67 | { label: '30 min', minutes: 30, seconds: 0 }, 68 | { label: '45 | 45', minutes: 45, seconds: 45 } 69 | ], 70 | showVariantsBox: false, 71 | showTimesBox: false, 72 | selectedType: 'daily' 73 | }; 74 | this.state.selectedVariant = this.state.variants[0]; 75 | this.state.selectedTime = this.state.dailyTimes[0]; 76 | this.toggleVariantsBox = this.toggleVariantsBox.bind(this); 77 | this.toggleTimesBox = this.toggleTimesBox.bind(this); 78 | this.changeVariant = this.changeVariant.bind(this); 79 | this.changeTime = this.changeTime.bind(this); 80 | this.isSelectedTime = this.isSelectedTime.bind(this); 81 | this.isSelectedVariant = this.isSelectedVariant.bind(this); 82 | this.playUrl = this.playUrl.bind(this); 83 | } 84 | 85 | playUrl() { 86 | let slug; 87 | if (this.state.selectedType === 'live') { 88 | slug = `live/?#s=${this.state.selectedTime.minutes}m${this.state.selectedTime.seconds}s`; 89 | } else { 90 | slug = `home/#d=${this.state.selectedTime.days}`; 91 | } 92 | if (this.state.selectedVariant.value !== 'chess') { 93 | slug += `|${this.state.selectedVariant.value}`; 94 | } 95 | return slug; 96 | } 97 | 98 | toggleVariantsBox() { 99 | this.setState({ showVariantsBox: !this.state.showVariantsBox, showTimesBox: false }); 100 | } 101 | 102 | toggleTimesBox() { 103 | this.setState({ showTimesBox: !this.state.showTimesBox, showVariantsBox: false }); 104 | } 105 | 106 | changeVariant(variant) { 107 | this.setState({ selectedVariant: variant }); 108 | this.closeBoxes(); 109 | } 110 | 111 | closeBoxes() { 112 | this.setState({ 113 | showVariantsBox: false, 114 | showTimesBox: false 115 | }); 116 | } 117 | 118 | isSelectedVariant(variant) { 119 | return lodash.isEqual(this.state.selectedVariant, variant); 120 | } 121 | 122 | isSelectedTime(time) { 123 | return lodash.isEqual(this.state.selectedTime, time); 124 | } 125 | 126 | changeTime(time) { 127 | const update = {}; 128 | if (lodash.find(this.state.dailyTimes, time)) { 129 | update.selectedType = 'daily'; 130 | } else { 131 | update.selectedType = 'live'; 132 | } 133 | // If the use has a variant incompatible with daily, revert to 134 | // standard 135 | if (time.days && !this.state.selectedVariant.daily) { 136 | this.setState({ selectedVariant: this.state.variants[0] }); 137 | } 138 | // Close the boxes 139 | this.closeBoxes(); 140 | update.selectedTime = time; 141 | this.setState(update); 142 | } 143 | 144 | render() { 145 | if (this.props.user.loggedIn) { 146 | return ( 147 | React.createElement(Play, { 148 | api: Object.assign({}, this.state, { 149 | handlePlay: this.handlePlay, 150 | toggleVariantsBox: this.toggleVariantsBox, 151 | toggleTimesBox: this.toggleTimesBox, 152 | changeVariant: this.changeVariant, 153 | changeTime: this.changeTime, 154 | isSelectedVariant: this.isSelectedVariant, 155 | isSelectedTime: this.isSelectedTime, 156 | playUrl: this.playUrl 157 | }) 158 | })); 159 | } 160 | return null; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /app/components/Reset.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Icon from './Icon'; 3 | import BaseComponent from '../BaseComponent'; 4 | 5 | export default class Reset extends BaseComponent { 6 | 7 | static propTypes = { 8 | type: PropTypes.oneOf(['all', 'color']).isRequired, 9 | className: PropTypes.string, 10 | iconProps: PropTypes.object.isRequired, 11 | selector: PropTypes.string.isRequired, 12 | colorName: (props, propName, componentName) => { 13 | // This is the name given in the Config file and used to 14 | // store the styles of this kind of node in the local storage 15 | if (props.type === 'color' && 16 | ((typeof props.colorName) !== 'string' || 17 | !props.colorName)) { 18 | return new Error(`Invalid prop '${propName}' supplied to \ 19 | '${componentName}'. Validation failed`); 20 | } 21 | }, 22 | colorProperty: (props, propName, componentName) => { 23 | // This is the name of the property in the inline-styling 24 | // that should be reset 25 | if (props.type === 'color' && 26 | ((typeof props.colorProperty) !== 'string' || 27 | !props.colorProperty)) { 28 | return new Error(`Invalid prop '${propName}' supplied to \ 29 | '${componentName}'. Validation failed`); 30 | } 31 | }, 32 | }; 33 | 34 | handleClick = () => { 35 | if (this.props.type === 'all') { 36 | chrome.storage.local.set({ 37 | style: {}, 38 | display: {}, 39 | fontFamily: '' 40 | }); 41 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 42 | chrome.tabs.sendMessage(tabs[0].id, { 43 | update: 'reset', 44 | selector: this.props.selector, 45 | }); 46 | }); 47 | } else if (this.props.type === 'color') { 48 | const name = this.props.colorName; 49 | chrome.storage.local.get('style', result => { 50 | delete result.style[name]; 51 | chrome.storage.local.set(result); 52 | }); 53 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 54 | chrome.tabs.sendMessage(tabs[0].id, { 55 | update: 'style', 56 | selector: this.props.selector, 57 | property: this.props.colorProperty, 58 | }); 59 | }); 60 | } 61 | } 62 | 63 | render() { 64 | return ( 65 |
66 | 67 | { this.props.type === 'all' ? 68 | Reset All 69 | : null } 70 |
71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /app/components/ResetBar.css: -------------------------------------------------------------------------------- 1 | .resetBar { 2 | background: #f1f1f1; 3 | } 4 | 5 | .resetBar div { 6 | display:inline-block; 7 | position:relative; 8 | margin-bottom: 3px; 9 | } 10 | 11 | .resetIcon { 12 | float: left; 13 | margin-right: 5px; 14 | } 15 | 16 | .resetButton { 17 | left: 5px; 18 | padding: 5px; 19 | width: 70%; 20 | } 21 | 22 | .resetButton > div { 23 | font-weight: bold; 24 | font-size: 12px; 25 | line-height: -20px; 26 | color: #8c8a88; 27 | } 28 | 29 | .resetButton > div i { 30 | position: relative; 31 | margin-right: 5px; 32 | top: 3px; 33 | } 34 | 35 | .resetButton > div:hover { 36 | cursor: pointer; 37 | color: rgb(80, 80, 80); 38 | } 39 | 40 | .suggestionsIcon { 41 | position: relative; 42 | top: -2px; 43 | float: left; 44 | margin-right: 3px; 45 | } 46 | 47 | .suggestions { 48 | color: #8c8a88; 49 | text-align: center; 50 | font-weight: bold; 51 | font-size: 12px; 52 | text-decoration: none; 53 | right: 5%; 54 | } 55 | 56 | .suggestions:hover { 57 | color: rgb(80, 80, 80); 58 | cursor: pointer; 59 | } 60 | 61 | .suggestions:focus { 62 | outline: none; 63 | } 64 | -------------------------------------------------------------------------------- /app/components/ResetBar.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import style from './ResetBar.css'; 3 | import Icon from './Icon.js'; 4 | import Config from './Options/Config'; 5 | import Reset from '../components/Reset'; 6 | import BaseComponent from '../BaseComponent'; 7 | 8 | export default class ResetBar extends BaseComponent { 9 | 10 | handleSuggestionsClick = () => { 11 | const suggestions = 'http://goo.gl/forms/AVaggClVWuIyP87k1'; 12 | chrome.tabs.create({ url: suggestions }); 13 | } 14 | 15 | render() { 16 | let resetAllSelector = ''; 17 | Config.groups.forEach(group => { 18 | group.options.forEach(option => { 19 | if (!Object.hasOwnProperty.call(option, 'selector')) { 20 | return; 21 | } 22 | const localSelectorArray = option.selector; 23 | if (resetAllSelector) { 24 | // Only do this if it's not the first time 25 | resetAllSelector += ','; 26 | } 27 | resetAllSelector += localSelectorArray.join(','); 28 | }); 29 | }); 30 | 31 | return ( 32 |
33 |
34 | 39 |
40 |
41 |
42 | 43 |
44 | Suggestions 45 |
46 |
47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/components/Times.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames/bind'; 3 | import Button from './Button'; 4 | import Icon from '../components/Icon'; 5 | import buttonStyle from '../components/Button.css'; 6 | import style from '../components/Play.css'; 7 | import BaseComponent from '../BaseComponent'; 8 | 9 | const cx = classNames.bind(style); 10 | 11 | export default class Times extends BaseComponent { 12 | 13 | static propTypes = { 14 | liveTimes: PropTypes.array.isRequired, 15 | dailyTimes: PropTypes.array.isRequired, 16 | onClick: PropTypes.func.isRequired, 17 | isSelectedTime: PropTypes.func.isRequired 18 | }; 19 | 20 | render() { 21 | const liveTimes = this.props.liveTimes.map((time, i) => { 22 | const className = cx({ 23 | [buttonStyle.selected]: this.props.isSelectedTime(time), 24 | [buttonStyle.small]: true 25 | }); 26 | 27 | return (); 33 | }); 34 | 35 | const dailyTimes = this.props.dailyTimes.map((time, i) => { 36 | const className = cx({ 37 | [buttonStyle.selected]: this.props.isSelectedTime(time), 38 | [buttonStyle.small]: true 39 | }); 40 | return (); 46 | }); 47 | 48 | return ( 49 |
50 |
51 |
52 | Live 53 |
54 | { liveTimes } 55 |
56 |
57 |
58 | Daily 59 |
60 | { dailyTimes } 61 |
62 |
63 | ); 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /app/components/Toggle.css: -------------------------------------------------------------------------------- 1 | .row { 2 | padding: 0.2rem 0; 3 | } 4 | 5 | .toggle { 6 | margin-top: 2px; 7 | width: 44px; 8 | height: 22px; 9 | background: #bfbeba; 10 | border: 1px solid #e8e7e6; 11 | position: relative; 12 | cursor: pointer; 13 | border-radius: 13px; 14 | float: right; 15 | } 16 | 17 | .active { 18 | background: #67A032; 19 | } 20 | 21 | .button { 22 | border-radius: 100%; 23 | width: 24px; 24 | height: 24px; 25 | margin: -2px; 26 | background: #e8e7e6; 27 | border: 1px solid #dbd9d7; 28 | position: absolute; 29 | transition: all .2s .2s ease-out; 30 | transition-delay: 0s; 31 | left: 0; 32 | } 33 | 34 | .active .button { 35 | left: 21px; 36 | } 37 | -------------------------------------------------------------------------------- /app/components/ToggleDisplay.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames/bind'; 3 | import style from './Toggle.css'; 4 | import BaseComponent from '../BaseComponent'; 5 | 6 | const cx = classNames.bind(style); 7 | 8 | export default class ToggleDisplay extends BaseComponent { 9 | 10 | static propTypes = { 11 | name: PropTypes.string.isRequired, 12 | title: PropTypes.string.isRequired, 13 | selector: PropTypes.string.isRequired, 14 | helpers: PropTypes.array 15 | }; 16 | 17 | constructor(props, context) { 18 | super(props, context); 19 | this.state = { 20 | update: 'display', 21 | name: this.props.name, 22 | selector: this.props.selector, 23 | visible: true, 24 | helpers: this.props.helpers 25 | }; 26 | this.checkIfStorageAlreadyExists(); 27 | } 28 | 29 | componentDidUpdate = () => { 30 | const name = this.props.name; 31 | const state = JSON.parse(JSON.stringify(this.state)); 32 | 33 | this.save(name, state); 34 | 35 | this.runHelpers(); 36 | this.sendMessageToDOM(); 37 | } 38 | 39 | save = (name, state) => { 40 | chrome.storage.local.get('display', result => { 41 | const obj = result; 42 | if (Object.keys(result).length === 0 && obj.constructor === Object) { 43 | obj.display = {}; 44 | } 45 | obj.display[name] = state; 46 | chrome.storage.local.set(obj); 47 | }); 48 | } 49 | 50 | checkIfStorageAlreadyExists() { 51 | const name = this.props.name; 52 | chrome.storage.local.get(result => { 53 | if (!{}.hasOwnProperty.call(result, 'display')) { 54 | return chrome.storage.local.set({ display: {} }); 55 | } 56 | 57 | if ({}.hasOwnProperty.call(result.display, name)) { 58 | this.setState({ visible: result.display[name].visible }); 59 | } 60 | }); 61 | } 62 | 63 | runHelpers = () => { 64 | if (this.state.helpers) { 65 | chrome.storage.local.get('display', result => { 66 | this.state.helpers.forEach(helper => { 67 | if (helper.type === 'hide') { 68 | if (!{}.hasOwnProperty.call(result.display, helper.relation)) { 69 | return; 70 | } 71 | const relation = result.display[helper.relation].visible; 72 | helper.display = (this.state.visible + relation) !== 0; 73 | this.sendHelperToDOM(helper); 74 | } 75 | }); 76 | }); 77 | } 78 | } 79 | 80 | handleClick = () => { 81 | const props = this.props; 82 | this.setState({ 83 | visible: !this.state.visible, 84 | selector: props.selector 85 | }); 86 | }; 87 | 88 | sendMessageToDOM = () => { 89 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 90 | chrome.tabs.sendMessage(tabs[0].id, { 91 | update: 'display', 92 | selector: this.props.selector, 93 | display: this.state.visible 94 | }); 95 | }); 96 | } 97 | 98 | sendHelperToDOM = (helper) => { 99 | const target = [this.state.name, helper.relation].sort(); 100 | const obj = { 101 | update: 'display', 102 | name: `helper-${helper.type}-${target[0]}-${target[1]}`, 103 | selector: helper.selector, 104 | visible: helper.display 105 | 106 | }; 107 | 108 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 109 | chrome.tabs.sendMessage(tabs[0].id, { 110 | update: obj.update, 111 | selector: obj.selector, 112 | display: obj.visible 113 | }); 114 | }); 115 | 116 | if (typeof obj.visible !== 'undefined') { 117 | this.save(obj.name, obj); 118 | } 119 | } 120 | 121 | render() { 122 | const toggle = cx({ 123 | toggle: true, 124 | active: !this.state.visible 125 | }); 126 | 127 | const inputId = `${this.props.name}_input`; 128 | 129 | return ( 130 |
131 | 132 |
137 |
138 |
139 |
140 | ); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/components/Variants.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import classNames from 'classnames/bind'; 3 | import Button from './Button'; 4 | import Icon from '../components/Icon'; 5 | import style from '../components/Play.css'; 6 | import buttonStyle from '../components/Button.css'; 7 | import BaseComponent from '../BaseComponent'; 8 | 9 | const cx = classNames.bind(style); 10 | 11 | export default class Variants extends BaseComponent { 12 | 13 | static propTypes = { 14 | variants: PropTypes.array.isRequired, 15 | selectedType: PropTypes.string.isRequired, 16 | onClick: PropTypes.func.isRequired, 17 | isSelectedVariant: PropTypes.func.isRequired 18 | }; 19 | 20 | render() { 21 | const variants = this.props.variants.map((type, i) => { 22 | const className = cx({ 23 | [buttonStyle.selected]: this.props.isSelectedVariant(type), 24 | [buttonStyle.panel]: true 25 | }); 26 | 27 | if (type[this.props.selectedType] === true) { 28 | return ( 29 | 42 | ); 43 | } 44 | return false; 45 | }); 46 | 47 | return (
{ variants }
); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/containers/App.css: -------------------------------------------------------------------------------- 1 | .app-base { 2 | background: #1d1c1a; 3 | } 4 | 5 | .app-bulk { 6 | /* Max height of extension is 600px, if we don't add our 7 | * our own max-height, a second scrollbar will appear when 8 | * the extension tries to exceed 600px. Notification bar is 45px 9 | * and Header is 55 so 600-45-55 = 500. 10 | */ 11 | max-height: 500px; 12 | overflow-y: auto; 13 | } 14 | 15 | .not-mac-os .app-bulk { 16 | /* If not MacOS we make vertical scroll bar always show 17 | * so as to avoid the extension being jumpy when overflow 18 | * sets in on expanding an option. 19 | */ 20 | overflow-y: scroll; 21 | } 22 | -------------------------------------------------------------------------------- /app/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Header from '../components/Header'; 3 | import NotificationBar from '../components/NotificationBar'; 4 | import ResetBar from '../components/ResetBar'; 5 | import PlayContainer from '../components/PlayContainer'; 6 | import Options from '../components/Options/Options'; 7 | import style from './App.css'; 8 | import BaseComponent from '../BaseComponent'; 9 | 10 | export default class App extends BaseComponent { 11 | 12 | static propTypes = { 13 | user: PropTypes.object.isRequired, 14 | notifications: PropTypes.object.isRequired 15 | }; 16 | 17 | render() { 18 | let notificationBar = (
); 19 | 20 | if (!this.props.user.loading) { 21 | if (this.props.user.loggedIn) { 22 | notificationBar = ( 23 | 24 | ); 25 | } 26 | } 27 | 28 | let appClassName = style['app-base']; 29 | if (this.props.os !== 'mac') { 30 | appClassName += ` ${style['not-mac-os']}`; 31 | } 32 | 33 | return ( 34 |
35 |
36 |
37 | 38 | 39 | 40 |
41 | { notificationBar } 42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/containers/Root.js: -------------------------------------------------------------------------------- 1 | import xhr from 'xhr'; 2 | import React from 'react'; 3 | import App from './App'; 4 | import BaseComponent from '../BaseComponent'; 5 | 6 | export default class Root extends BaseComponent { 7 | 8 | constructor() { 9 | super(); 10 | this.state = { 11 | user: { 12 | loading: true, 13 | onChessCom: null, 14 | loggedIn: null 15 | }, 16 | notifications: { 17 | loading: true, 18 | games: '', 19 | messages: '', 20 | alerts: '' 21 | } 22 | }; 23 | } 24 | 25 | componentDidMount() { 26 | chrome.storage.local.get('notifications', result => { 27 | const data = Object.assign({}, result.notifications); 28 | data.loading = false; 29 | this.setState({ notifications: data }); 30 | }); 31 | 32 | const user = this.state.user; 33 | 34 | this.calcLoggedIn(user).then((user1) => { 35 | if (user1.loggedIn) { 36 | if (!user1.avatarUrl) { 37 | this.setAvatar(user1).then((user2) => { 38 | this.resolveUser(user2); 39 | // Save the avatar to localStorage so we can cache it 40 | chrome.storage.local.set({ user: user2 }); 41 | }); 42 | } else { 43 | this.resolveUser(user1); 44 | } 45 | } else { 46 | this.calcOnChessCom(user1).then((user2) => { 47 | this.resolveUser(user2); 48 | }); 49 | } 50 | }); 51 | } 52 | 53 | /** 54 | * This should only be called if we know for a fact that the 55 | * user is logged in and their avatar has not yet been cached 56 | * 57 | * @return Promise 58 | */ 59 | setAvatar(user) { 60 | return new Promise(resolve => 61 | xhr.get(`https://www.chess.com/callback/user/popup/${user.username}`, 62 | { json: true }, 63 | (err, resp) => { 64 | if (resp.statusCode === 200) { 65 | return resolve(Object.assign({}, user, { avatarUrl: resp.body.avatarUrl })); 66 | } 67 | resolve(user); 68 | }) 69 | ); 70 | } 71 | 72 | /** 73 | * @return Promise 74 | */ 75 | calcLoggedIn(user) { 76 | return new Promise(resolve => 77 | chrome.storage.local.get('user', (result) => { 78 | if (result.user) { 79 | // Add payload and loggedIn property to user 80 | resolve(Object.assign({}, user, result.user, { loggedIn: true })); 81 | } else { 82 | resolve(Object.assign({}, user, { loggedIn: false })); 83 | } 84 | })); 85 | } 86 | 87 | /** 88 | * Earmark the user as currently on Chess.com or not 89 | * 90 | * @return Promise 91 | */ 92 | calcOnChessCom(user) { 93 | return new Promise(resolve => 94 | chrome.tabs.query({ active: true, currentWindow: true }, tabs => { 95 | if (tabs[0].url.indexOf('chess.com') >= 0) { 96 | resolve(Object.assign({}, user, { onChessCom: true })); 97 | } else { 98 | resolve(Object.assign({}, user, { onChessCom: false })); 99 | } 100 | })); 101 | } 102 | 103 | /** 104 | * Once a promise is fulfilled we should call this function. This is 105 | * the only place where we set user.loading to false. 106 | * 107 | * @return promise 108 | */ 109 | resolveUser(user) { 110 | const userInfoComplete = user.avatarUrl; 111 | if (userInfoComplete || user.onChessCom === false || 112 | (user.onChessCom !== null && user.loggedIn !== null)) { 113 | const userToSave = Object.assign({}, user, { loading: false }); 114 | this.setState({ user: userToSave }); 115 | } 116 | } 117 | 118 | render() { 119 | return ( 120 | 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /chrome/assets/fonts/chessglyph-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/fonts/chessglyph-regular.woff -------------------------------------------------------------------------------- /chrome/assets/img/arrow-down-medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/arrow-down-medium.png -------------------------------------------------------------------------------- /chrome/assets/img/arrow-down-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/arrow-down-small.png -------------------------------------------------------------------------------- /chrome/assets/img/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/home.png -------------------------------------------------------------------------------- /chrome/assets/img/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/icon-128.png -------------------------------------------------------------------------------- /chrome/assets/img/icon-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/icon-16.png -------------------------------------------------------------------------------- /chrome/assets/img/icon-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/icon-48.png -------------------------------------------------------------------------------- /chrome/assets/img/interface-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/interface-icon.png -------------------------------------------------------------------------------- /chrome/assets/img/logo-black.svg: -------------------------------------------------------------------------------- 1 | Slice 1 -------------------------------------------------------------------------------- /chrome/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/logo.png -------------------------------------------------------------------------------- /chrome/assets/img/logo.svg: -------------------------------------------------------------------------------- 1 | Slice 1 -------------------------------------------------------------------------------- /chrome/assets/img/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ChessCom/browser-extension/552e791d3f15b2dcbb80974cd582f06d922fc07e/chrome/assets/img/sprite.png -------------------------------------------------------------------------------- /chrome/extension/background.js: -------------------------------------------------------------------------------- 1 | function loadScript(name, tabId, cb) { 2 | if (process.env.NODE_ENV === 'production') { 3 | chrome.tabs.executeScript(tabId, { 4 | file: `/js/${name}.bundle.js`, 5 | runAt: 'document_start' 6 | }, 7 | cb); 8 | } else { 9 | // dev: async fetch bundle 10 | fetch(`https://localhost:3000/js/${name}.bundle.js`) 11 | .then(res => res.text()) 12 | .then(fetchRes => { 13 | chrome.tabs.executeScript(tabId, { code: fetchRes, runAt: 'document_end' }, cb); 14 | }); 15 | } 16 | } 17 | 18 | function jsNameToCssName(name) { 19 | return name.replace(/([A-Z])/g, '-$1').toLowerCase(); 20 | } 21 | 22 | function injectCSS(tabId, style) { 23 | Object.keys(style).forEach(key => { 24 | const prop = style[key]; 25 | const color = prop.color; 26 | const property = jsNameToCssName(prop.property); 27 | const rgba = `rgba(${color.r},${color.g},${color.b},${color.a})`; 28 | let selector = prop.selector; 29 | if (selector) { 30 | selector = selector.split(',').map(singleSelector => ( 31 | `body.removable-initial-styles ${singleSelector}` 32 | )).join(','); 33 | } 34 | const css = `${selector} { ${property}: ${rgba} }`; 35 | chrome.tabs.insertCSS(tabId, { 36 | code: css, 37 | runAt: 'document_start' 38 | }); 39 | }); 40 | } 41 | 42 | function injectDisplay(tabId, display) { 43 | Object.keys(display).forEach(key => { 44 | const prop = display[key]; 45 | const visible = prop.visible ? 'block' : 'none'; 46 | let selector = prop.selector; 47 | if (selector) { 48 | selector = selector.split(',').map(singleSelector => ( 49 | `body.removable-initial-styles ${singleSelector}` 50 | )).join(','); 51 | } 52 | const css = `${selector} { display: ${visible} }`; 53 | chrome.tabs.insertCSS(tabId, { 54 | code: css, 55 | runAt: 'document_start' 56 | }); 57 | }); 58 | } 59 | 60 | function injectFontFamily(tabId, fontFamily) { 61 | const code = `body.removable-initial-styles { font-family: ${fontFamily} !important }`; 62 | chrome.tabs.insertCSS(tabId, { 63 | code, 64 | runAt: 'document_start' 65 | }); 66 | } 67 | 68 | function injectTempStylesClass(tabId) { 69 | // We use a mutation observer to be able to modify the DOM 70 | // as soon as the body element is created but before anything is 71 | // rendered so as to avoid FOUC 72 | const code = (` 73 | const observer = new MutationObserver(function(mutations) { 74 | // Use .some instead of .forEach as .forEach can't be cancelled and 75 | // .some cancels as soon as a truthy return value is given 76 | mutations.some(function(mutation) { 77 | if (mutation.target.nodeName === 'HTML' && mutation.addedNodes && 78 | mutation.addedNodes.length && mutation.addedNodes[0].nodeName === 'BODY') { 79 | // The body element was created in the dom 80 | const body = mutation.addedNodes[0]; 81 | body.classList.add('removable-initial-styles'); 82 | // Stop observer listening 83 | observer.disconnect(); 84 | // Stop the loop by returning true to .some 85 | return true; 86 | } 87 | }); 88 | }); 89 | 90 | const config = {childList: true, subtree: true}; 91 | observer.observe(document, config); 92 | `); 93 | 94 | chrome.tabs.executeScript(tabId, { code, runAt: 'document_start' }); 95 | } 96 | 97 | function updateBadge() { 98 | chrome.runtime.onMessage.addListener((request) => { 99 | if (request.badge || request.badge === 0) { 100 | const total = request.badge.toString(); 101 | // Use red background for now 102 | chrome.browserAction.setBadgeBackgroundColor({ color: [179, 52, 48, 50] }); 103 | if (total >= 1) { 104 | chrome.browserAction.setBadgeText({ text: total }); 105 | } else { 106 | chrome.browserAction.setBadgeText({ text: '' }); 107 | } 108 | } 109 | }); 110 | } 111 | 112 | function onLoading(tabId) { 113 | // Get cached styles to inject before the DOM loads on each page load 114 | chrome.storage.local.get(storage => { 115 | injectCSS(tabId, storage.style || {}); 116 | injectDisplay(tabId, storage.display || {}); 117 | injectFontFamily(tabId, storage.fontFamily || ''); 118 | }); 119 | // Insert the temp styles class in body so initial css shows 120 | injectTempStylesClass(tabId); 121 | 122 | updateBadge(); 123 | 124 | if (chrome.runtime.lastError) return; 125 | 126 | // Loads content script to manipulate the dom in real time 127 | loadScript('handleLiveChanges', tabId); 128 | } 129 | 130 | const arrowURLs = ['^https://www\\.chess\\.com']; 131 | 132 | chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { 133 | if (tab.url.match(arrowURLs.join('|'))) { 134 | if (changeInfo.status === 'loading') { 135 | onLoading(tabId); 136 | } 137 | } 138 | }); 139 | -------------------------------------------------------------------------------- /chrome/extension/handleLiveChanges.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | function jsNameToCssName(name) { 4 | return name.replace(/([A-Z])/g, '-$1').toLowerCase(); 5 | } 6 | 7 | function updateStyles() { 8 | // Handles live update from color picker 9 | chrome.runtime.onMessage.addListener( 10 | (request, sender) => { 11 | if (!sender.tab) { 12 | if (request.update === 'style') { 13 | const { property, selector, color } = request; 14 | const elementsArray = document.querySelectorAll(selector); 15 | 16 | let rgba = ''; 17 | if (color) { 18 | rgba = `rgba(${color.r},${color.g},${color.b},${color.a})`; 19 | } 20 | 21 | elementsArray.forEach(element => { 22 | element.style[property] = rgba; 23 | }); 24 | } 25 | } 26 | } 27 | ); 28 | } 29 | 30 | function updateDisplay() { 31 | chrome.runtime.onMessage.addListener( 32 | (request, sender) => { 33 | if (!sender.tab) { 34 | if (request.update === 'display') { 35 | const elementsArray = document.querySelectorAll(request.selector); 36 | try { 37 | elementsArray.forEach(element => { 38 | if (!request.display) { 39 | element.style.display = 'none'; 40 | } else { 41 | element.style.display = 'block'; 42 | } 43 | }); 44 | } catch (e) { 45 | throw e; 46 | } 47 | } 48 | } 49 | } 50 | ); 51 | } 52 | 53 | function updateFontFamily() { 54 | chrome.runtime.onMessage.addListener( 55 | (request, sender) => { 56 | if (!sender.tab) { 57 | if (request.update === 'fontFamily') { 58 | try { 59 | document.body.style.fontFamily = `${request.font} !important`; 60 | } catch (e) { 61 | throw e; 62 | } 63 | } 64 | } 65 | } 66 | ); 67 | } 68 | 69 | function reset() { 70 | // Resets all styles on all elements given in selector 71 | chrome.runtime.onMessage.addListener( 72 | (request, sender) => { 73 | if (!sender.tab) { 74 | if (request.update === 'reset') { 75 | const elementsArray = document.querySelectorAll(request.selector); 76 | elementsArray.forEach(element => { 77 | element.style = ''; 78 | }); 79 | } 80 | } 81 | } 82 | ); 83 | } 84 | 85 | function sendNotification(total, cb) { 86 | return chrome.runtime.sendMessage({ 87 | badge: total 88 | }, cb); 89 | } 90 | 91 | function getNotifications() { 92 | // Set a delay for getting notifcations 93 | setTimeout(() => { 94 | const elementsArray = document.querySelectorAll('span[data-notifications]'); 95 | const nodes = [...elementsArray].splice(0, 3); 96 | let total = 0; 97 | 98 | const notifications = { 99 | games: '', 100 | messages: '', 101 | alerts: '' 102 | }; 103 | const notificationKeys = Object.keys(notifications); 104 | 105 | nodes.map((target, index) => { 106 | const value = parseInt(target.dataset.notifications, 10); 107 | total += value; 108 | if (value !== 0) { 109 | notifications[notificationKeys[index]] = parseInt(target.dataset.notifications, 10); 110 | } 111 | return total; 112 | }); 113 | 114 | chrome.storage.local.set({ notifications }); 115 | return sendNotification(total, getNotifications); 116 | }, 60000); 117 | } 118 | 119 | function onLoadComplete() { 120 | // We make all the injected styles inline for easier 121 | // manipulation and then remove the removable-initial-styles class from body 122 | chrome.storage.local.get(storage => { 123 | const { style, display, fontFamily } = storage; 124 | // Browsers should batch all these DOM updates as they are all consecutive 125 | // First insert inline-styles 126 | if (style) { 127 | _.forEach(style, updateObject => { 128 | const { color, selector } = updateObject; 129 | const property = jsNameToCssName(updateObject.property); 130 | const rgba = `rgba(${color.r},${color.g},${color.b},${color.a})`; 131 | const elementsArray = document.querySelectorAll(selector); 132 | elementsArray.forEach(element => { 133 | element.style[property] = rgba; 134 | }); 135 | }); 136 | } 137 | // Then we insert inline-display 138 | if (display) { 139 | _.forEach(display, updateObject => { 140 | const visible = updateObject.visible ? 'block' : 'none'; 141 | const elementsArray = document.querySelectorAll(updateObject.selector); 142 | elementsArray.forEach(element => { 143 | element.style.display = visible; 144 | }); 145 | }); 146 | } 147 | // Finally insert inline-fontFamily 148 | if (fontFamily) { 149 | document.body.style.fontFamily = `${fontFamily} !important`; 150 | } 151 | 152 | // When all inline-styles are applied we can remove the class from the body 153 | document.body.classList.remove('removable-initial-styles'); 154 | }); 155 | } 156 | 157 | window.addEventListener('load', () => { 158 | updateStyles(); 159 | updateDisplay(); 160 | updateFontFamily(); 161 | reset(); 162 | onLoadComplete(); 163 | 164 | /** 165 | * Set a delay whilst we wait for current page to compute all DOM 166 | * elements that have notification attributes bound to them 167 | */ 168 | setTimeout(getNotifications, 1000); 169 | }); 170 | 171 | // This listener catches the message from chrome/getCurrentUser.js 172 | // which contains the current logged in user and then stores it locally 173 | // so our extension knows we are logged in 174 | window.addEventListener('message', (event) => { 175 | // We only accept messages from ourselves 176 | if (event.source !== window) { 177 | return; 178 | } 179 | 180 | if (event.data.username) { 181 | chrome.storage.local.set({ user: event.data }); 182 | } 183 | }, false); 184 | -------------------------------------------------------------------------------- /chrome/extension/popup.css: -------------------------------------------------------------------------------- 1 | *, 2 | html, 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | button { 9 | margin: 0; 10 | padding: 0; 11 | border: 0; 12 | background: none; 13 | font-size: 100%; 14 | vertical-align: baseline; 15 | font-family: inherit; 16 | font-weight: inherit; 17 | color: inherit; 18 | appearance: none; 19 | font-smoothing: antialiased; 20 | } 21 | 22 | body { 23 | font: 14px 'Open Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif; 24 | line-height: 1.4em; 25 | background: #f5f5f5; 26 | color: #4d4d4d; 27 | width: 320px; 28 | margin: 0 auto; 29 | } 30 | 31 | /* This is more specific and put after 32 | * the body so should override with specifics 33 | * for OSX styling 34 | */ 35 | body.is-mac-os { 36 | width: 300px; 37 | } 38 | 39 | button, 40 | input[type="checkbox"] { 41 | outline: none; 42 | } 43 | 44 | * { 45 | -webkit-box-sizing: border-box; 46 | -moz-box-sizing: border-box; 47 | box-sizing: border-box; 48 | } 49 | -------------------------------------------------------------------------------- /chrome/extension/popup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import Root from '../../app/containers/Root'; 4 | import styles from './popup.css'; 5 | 6 | // Get platformInfo before rendering Root 7 | chrome.runtime.getPlatformInfo(platformInfo => { 8 | if (platformInfo.os === 'mac') { 9 | document.body.classList.add(styles['is-mac-os']); 10 | } 11 | ReactDOM.render( 12 | , 13 | document.querySelector('#root') 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /chrome/getCurrentUser.js: -------------------------------------------------------------------------------- 1 | if (window.context) { 2 | // Chess.com sets the logged in user data on window.context 3 | // which we fetch from inside the actual website and send to 4 | // the extension through postMessage 5 | window.postMessage(window.context.user, '*'); 6 | // This lets Chess.com know that this user is using the extension 7 | // which can help with troubleshooting / data handling etc. 8 | window.context.chessBrowserExtension = true; 9 | } 10 | -------------------------------------------------------------------------------- /chrome/injectScriptWithWindowAccess.js: -------------------------------------------------------------------------------- 1 | /** 2 | * injectScript - Inject internal script to available access to the `window` 3 | * 4 | * @param {type} file_path Local path of the internal script. 5 | * @param {type} tag The tag as string, where the script will be append (default: 'body'). 6 | * @see {@link http://stackoverflow.com/questions/20499994/access-window-variable-from-content-script} 7 | */ 8 | function injectScript(filePath, tag) { 9 | const node = document.getElementsByTagName(tag)[0]; 10 | const script = document.createElement('script'); 11 | script.setAttribute('type', 'text/javascript'); 12 | script.setAttribute('src', filePath); 13 | node.appendChild(script); 14 | } 15 | injectScript(chrome.extension.getURL('getCurrentUser.js'), 'body'); 16 | -------------------------------------------------------------------------------- /chrome/manifest.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "Chess Browser Extension", 4 | "manifest_version": 2, 5 | "description": "Customize your Chess.com Experience", 6 | "browser_action": { 7 | "default_title": "Chess.com Browser Extension", 8 | "default_popup": "popup.html" 9 | }, 10 | "icons": { 11 | "16": "img/icon-16.png", 12 | "48": "img/icon-48.png", 13 | "128": "img/icon-128.png" 14 | }, 15 | "web_accessible_resources": [ 16 | "getCurrentUser.js" 17 | ], 18 | "content_scripts": [ 19 | { 20 | "matches": ["https://www.chess.com/*"], 21 | "js": ["injectScriptWithWindowAccess.js"], 22 | "all_frames": true 23 | } 24 | ], 25 | "background": { 26 | "page": "background.html" 27 | }, 28 | "permissions": [ "contextMenus", "tabs", "storage", "https://www.chess.com/*" ], 29 | "content_security_policy": "default-src 'self'; script-src 'self'; connect-src https://www.chess.com; style-src * 'unsafe-inline'; img-src 'self' https://images.chesscomfiles.com data:; font-src 'self' data:;" 30 | } 31 | -------------------------------------------------------------------------------- /chrome/manifest.buildFirefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "Chess Browser Extension", 4 | "manifest_version": 2, 5 | "description": "Customize your Chess.com Experience", 6 | "browser_action": { 7 | "default_title": "Chess.com Browser Extension", 8 | "default_popup": "popup.html", 9 | "default_icon": { 10 | "48": "img/icon-48.png" 11 | } 12 | }, 13 | "icons": { 14 | "16": "img/icon-16.png", 15 | "48": "img/icon-48.png", 16 | "128": "img/icon-128.png" 17 | }, 18 | "applications": { 19 | "gecko": { 20 | "id": "chesscom@gmail.com" 21 | } 22 | }, 23 | "web_accessible_resources": [ 24 | "getCurrentUser.js" 25 | ], 26 | "content_scripts": [ 27 | { 28 | "matches": ["https://www.chess.com/*"], 29 | "js": ["injectScriptWithWindowAccess.js"], 30 | "all_frames": true 31 | } 32 | ], 33 | "background": { 34 | "page": "background.html" 35 | }, 36 | "permissions": [ "contextMenus", "tabs", "storage", "https://www.chess.com/*" ], 37 | "content_security_policy": "default-src 'self'; script-src 'self'; connect-src https://www.chess.com; style-src * 'unsafe-inline'; img-src 'self' https://images.chesscomfiles.com data:; font-src 'self' data:;" 38 | } 39 | -------------------------------------------------------------------------------- /chrome/manifest.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "Chess Browser Extension", 4 | "manifest_version": 2, 5 | "description": "Customize your Chess.com Experience", 6 | "browser_action": { 7 | "default_title": "Chess.com Browser Extension", 8 | "default_popup": "popup.html" 9 | }, 10 | "icons": { 11 | "128": "img/icon-128.png" 12 | }, 13 | "web_accessible_resources": [ 14 | "img/*", 15 | "fonts/*", 16 | "getCurrentUser.js" 17 | ], 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "https://www.chess.com/*" 22 | ], 23 | "js": [ 24 | "injectScriptWithWindowAccess.js" 25 | ], 26 | "all_frames": true 27 | } 28 | ], 29 | "background": { 30 | "page": "background.html" 31 | }, 32 | "permissions": [ 33 | "contextMenus", 34 | "management", 35 | "tabs", 36 | "storage", 37 | "https://www.chess.com/*" 38 | ], 39 | "content_security_policy": "default-src 'self'; script-src 'self' https://localhost:3000 'unsafe-eval'; connect-src https://localhost:3000 https://www.chess.com; style-src * 'unsafe-inline' 'self' blob:; img-src 'self' https://images.chesscomfiles.com data:; font-src 'self' data:;" 40 | } 41 | -------------------------------------------------------------------------------- /chrome/manifest.devFirefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "name": "Chess Browser Extension", 4 | "manifest_version": 2, 5 | "description": "Customize your Chess.com Experience", 6 | "browser_action": { 7 | "default_title": "Chess.com Browser Extension", 8 | "default_popup": "popup.html", 9 | "default_icon": { 10 | "48": "img/icon-48.png" 11 | } 12 | }, 13 | "icons": { 14 | "128": "img/icon-128.png" 15 | }, 16 | "applications": { 17 | "gecko": { 18 | "id": "chesscom@gmail.com" 19 | } 20 | }, 21 | "web_accessible_resources": [ 22 | "img/*", 23 | "fonts/*", 24 | "getCurrentUser.js" 25 | ], 26 | "content_scripts": [ 27 | { 28 | "matches": [ 29 | "https://www.chess.com/*" 30 | ], 31 | "js": [ 32 | "injectScriptWithWindowAccess.js" 33 | ], 34 | "all_frames": true 35 | } 36 | ], 37 | "background": { 38 | "page": "background.html" 39 | }, 40 | "permissions": [ 41 | "contextMenus", 42 | "management", 43 | "tabs", 44 | "storage", 45 | "https://www.chess.com/*" 46 | ], 47 | "content_security_policy": "default-src 'self'; script-src 'self' https://localhost:3000 'unsafe-eval'; object-src 'self'; connect-src https://localhost:3000 https://www.chess.com; style-src 'self' blob:; img-src 'self' https://images.chesscomfiles.com data:; font-src 'self' data:;" 48 | } 49 | -------------------------------------------------------------------------------- /chrome/views/background.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | script(src=env == 'prod' ? '/js/background.bundle.js' : 'https://localhost:3000/js/background.bundle.js') 6 | -------------------------------------------------------------------------------- /chrome/views/popup.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | 3 | html 4 | head 5 | meta(charset='UTF-8') 6 | title Chess.com Browser Extension 7 | 8 | body 9 | #root 10 | script(src=env == 'prod' ? '/js/popup.bundle.js' : 'https://localhost:3000/js/popup.bundle.js') 11 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | environment: 3 | PATH: "${PATH}:${HOME}/${CIRCLE_PROJECT_REPONAME}/node_modules/.bin" 4 | 5 | dependencies: 6 | override: 7 | - yarn 8 | cache_directories: 9 | - ~/.cache/yarn 10 | 11 | test: 12 | override: 13 | - echo "Overriding tests" 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chess-browser-extension", 3 | "version": "1.0.0", 4 | "description": "Chess.com Browser Extension", 5 | "scripts": { 6 | "dev": "node scripts/dev", 7 | "build": "node scripts/build", 8 | "devFirefox": "node scripts/devFirefox", 9 | "buildFirefox": "node scripts/buildFirefox", 10 | "compress": "node scripts/compress", 11 | "compress-keygen": "crx keygen", 12 | "clean": "rimraf build/ dev/ *.zip *.crx", 13 | "lint": "eslint app chrome test scripts webpack/*.js", 14 | "test-e2e": "cross-env NODE_ENV=test mocha test/e2e", 15 | "test": "cross-env NODE_ENV=test mocha -r ./test/setup-app test/app" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/ChessCom/browser-extension.git" 20 | }, 21 | "keywords": [ 22 | "chrome", 23 | "browser", 24 | "extension", 25 | "chess" 26 | ], 27 | "author": "Chess.com", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-core": "^6.3.15", 31 | "babel-eslint": "^6.0.0", 32 | "babel-loader": "^6.2.0", 33 | "babel-plugin-add-module-exports": "^0.2.1", 34 | "babel-plugin-transform-decorators-legacy": "^1.2.0", 35 | "babel-plugin-transform-runtime": "^6.5.2", 36 | "babel-plugin-webpack-loaders": "^0.7.0", 37 | "babel-preset-es2015": "^6.3.13", 38 | "babel-preset-react": "^6.3.13", 39 | "babel-preset-react-hmre": "^1.0.0", 40 | "babel-preset-stage-0": "^6.3.13", 41 | "babel-runtime": "^6.3.19", 42 | "chai": "^3.2.0", 43 | "chromedriver": "^2.19.0", 44 | "co-mocha": "^1.1.2", 45 | "cross-env": "^1.0.7", 46 | "crx": "^3.0.3", 47 | "css-loader": "^0.23.1", 48 | "eslint": "^3.4.0", 49 | "eslint-config-airbnb": "^10.0.1", 50 | "eslint-plugin-import": "^1.6.1", 51 | "eslint-plugin-jsx-a11y": "^2.2.0", 52 | "eslint-plugin-react": "^6.2.0", 53 | "extract-text-webpack-plugin": "^1.0.1", 54 | "file-loader": "^0.9.0", 55 | "jade": "^1.11.0", 56 | "jsdom": "^9.2.1", 57 | "minimist": "^1.2.0", 58 | "mocha": "^2.4.5", 59 | "postcss-loader": "^0.9.1", 60 | "react-addons-test-utils": "^15.0.2", 61 | "rimraf": "^2.4.3", 62 | "selenium-webdriver": "^2.47.0", 63 | "shelljs": "^0.7.0", 64 | "sinon": "^1.17.1", 65 | "style-loader": "^0.13.1", 66 | "url-loader": "^0.5.7", 67 | "webpack": "^1.13.0", 68 | "webpack-hot-middleware": "^2.10.0", 69 | "webpack-httpolyglot-server": "^0.2.0" 70 | }, 71 | "dependencies": { 72 | "classnames": "^2.2.5", 73 | "lodash": "^4.15.0", 74 | "react": "^15.0.2", 75 | "react-color": "^2.2.0", 76 | "react-dock": "^0.2.3", 77 | "react-dom": "^15.0.2", 78 | "reactcss": "^1.0.6", 79 | "xhr": "^2.2.2" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /scripts/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "shelljs": true 4 | }, 5 | "rules": { 6 | "no-console": 0 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const tasks = require('./tasks'); 2 | 3 | tasks.replaceWebpack(); 4 | console.log('[Copy assets]'); 5 | console.log('-'.repeat(80)); 6 | tasks.copyAssets('build'); 7 | 8 | console.log('[Webpack Build]'); 9 | console.log('-'.repeat(80)); 10 | exec('webpack --config webpack/prod.config.js --progress --profile --colors'); 11 | -------------------------------------------------------------------------------- /scripts/buildFirefox.js: -------------------------------------------------------------------------------- 1 | const tasks = require('./tasks'); 2 | 3 | tasks.replaceWebpack(); 4 | console.log('[Copy assets]'); 5 | console.log('-'.repeat(80)); 6 | tasks.copyAssets('buildFirefox'); 7 | 8 | console.log('[Webpack Build]'); 9 | console.log('-'.repeat(80)); 10 | exec('webpack --config webpack/prodFirefox.config.js --progress --profile --colors'); 11 | -------------------------------------------------------------------------------- /scripts/compress.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const fs = require('fs'); 3 | const ChromeExtension = require('crx'); 4 | /* eslint import/no-unresolved: 0 */ 5 | const name = require('../build/manifest.json').name; 6 | const argv = require('minimist')(process.argv.slice(2)); 7 | 8 | const keyPath = argv.key || 'key.pem'; 9 | const existsKey = fs.existsSync(keyPath); 10 | const crx = new ChromeExtension({ 11 | appId: argv['app-id'], 12 | codebase: argv.codebase, 13 | privateKey: existsKey ? fs.readFileSync(keyPath) : null 14 | }); 15 | 16 | crx.load('build') 17 | .then(() => crx.loadContents()) 18 | .then(archiveBuffer => { 19 | fs.writeFile(`${name}.zip`, archiveBuffer); 20 | 21 | if (!argv.codebase || !existsKey) return; 22 | crx.pack(archiveBuffer).then(crxBuffer => { 23 | const updateXML = crx.generateUpdateXML(); 24 | 25 | fs.writeFile('update.xml', updateXML); 26 | fs.writeFile(`${name}.crx`, crxBuffer); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /scripts/dev.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const tasks = require('./tasks'); 3 | const createWebpackServer = require('webpack-httpolyglot-server'); 4 | const devConfig = require('../webpack/dev.config'); 5 | 6 | tasks.replaceWebpack(); 7 | console.log('[Copy assets]'); 8 | console.log('-'.repeat(80)); 9 | tasks.copyAssets('dev'); 10 | 11 | console.log('[Webpack Dev]'); 12 | console.log('-'.repeat(80)); 13 | console.log('In order to allow access to inject.js'); 14 | console.log('please allow `https://localhost:3000` connections in Google Chrome,'); 15 | console.log('which can be done by enabling #allow-insecure-localhost at chrome://flags'); 16 | console.log('and load unpacked extensions with `./dev` folder. (see https://developer.chrome.com/extensions/getstarted#unpacked)\n'); 17 | createWebpackServer(devConfig, { 18 | host: 'localhost', 19 | port: 3000, 20 | protocol: 'https' 21 | }); 22 | -------------------------------------------------------------------------------- /scripts/devFirefox.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const tasks = require('./tasks'); 3 | const createWebpackServer = require('webpack-httpolyglot-server'); 4 | const devConfigFirefox = require('../webpack/devFirefox.config'); 5 | 6 | tasks.replaceWebpack(); 7 | console.log('[Copy assets]'); 8 | console.log('-'.repeat(80)); 9 | tasks.copyAssets('devFirefox'); 10 | 11 | console.log('[Webpack Dev]'); 12 | console.log('-'.repeat(80)); 13 | console.log('If you\'re developing Inject page,'); 14 | console.log('please allow `https://localhost:3000` connections in Firefox,'); 15 | console.log('and load unpacked extensions with `./devFirefox` folder. (see https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Your_first_WebExtension)\n'); 16 | createWebpackServer(devConfigFirefox, { 17 | host: 'localhost', 18 | port: 3000, 19 | protocol: 'https' 20 | }); 21 | -------------------------------------------------------------------------------- /scripts/tasks.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | require('shelljs/global'); 3 | 4 | exports.replaceWebpack = () => { 5 | const replaceTasks = [{ 6 | from: 'webpack/replace/JsonpMainTemplate.runtime.js', 7 | to: 'node_modules/webpack/lib/JsonpMainTemplate.runtime.js' 8 | }, { 9 | from: 'webpack/replace/log-apply-result.js', 10 | to: 'node_modules/webpack/hot/log-apply-result.js' 11 | }]; 12 | 13 | replaceTasks.forEach(task => cp(task.from, task.to)); 14 | }; 15 | 16 | exports.copyAssets = type => { 17 | let env = type; 18 | 19 | if (type.indexOf('build') >= 0) { 20 | env = 'prod'; 21 | } 22 | 23 | rm('-rf', type); 24 | mkdir(type); 25 | cp(`chrome/manifest.${type}.json`, `${type}/manifest.json`); 26 | cp('-R', 'chrome/assets/*', type); 27 | cp('-R', 'chrome/*.js', type); 28 | exec(`jade -O "{ env: '${env}' }" -o ${type} chrome/views/`); 29 | }; 30 | -------------------------------------------------------------------------------- /webpack/customPublicPath.js: -------------------------------------------------------------------------------- 1 | /* global __webpack_public_path__ __HOST__ __PORT__ */ 2 | /* eslint no-native-reassign: 0, camelcase: 0, no-global-assign: 0 */ 3 | 4 | if (process.env.NODE_ENV === 'production') { 5 | __webpack_public_path__ = chrome.extension.getURL('/js'); 6 | } else { 7 | // In development mode, 8 | // the iframe of injectpage cannot get correct path, 9 | // it need to get parent page protocol. 10 | const path = `//${__HOST__}:${__PORT__}/js`; 11 | if (location.protocol === 'https:' || location.search.indexOf('protocol=https') !== -1) { 12 | __webpack_public_path__ = `https:${path}`; 13 | } else { 14 | __webpack_public_path__ = `https:${path}`; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /webpack/dev.config.base.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const host = 'localhost'; 6 | const port = 3000; 7 | const customPath = path.join(__dirname, './customPublicPath'); 8 | const hotScript = 'webpack-hot-middleware/client?path=/__webpack_hmr&dynamicPublicPath=true'; 9 | 10 | const createDevConfig = (platform) => { 11 | const config = {}; 12 | if (platform === 'firefox') { 13 | config.outputDir = '../devFirefox/js'; 14 | } else { 15 | config.outputDir = '../dev/js'; 16 | } 17 | const baseDevConfig = () => ({ 18 | devtool: 'eval-source-map', 19 | entry: { 20 | popup: [customPath, hotScript, path.join(__dirname, '../chrome/extension/popup')], 21 | background: [customPath, hotScript, path.join(__dirname, '../chrome/extension/background')], 22 | }, 23 | devMiddleware: { 24 | publicPath: `https://${host}:${port}/js`, 25 | stats: { 26 | colors: true 27 | }, 28 | noInfo: true 29 | }, 30 | hotMiddleware: { 31 | path: '/js/__webpack_hmr' 32 | }, 33 | output: { 34 | path: path.join(__dirname, config.outputDir), 35 | filename: '[name].bundle.js', 36 | chunkFilename: '[id].chunk.js' 37 | }, 38 | plugins: [ 39 | new webpack.HotModuleReplacementPlugin(), 40 | new webpack.NoErrorsPlugin(), 41 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.prod$/), 42 | new webpack.DefinePlugin({ 43 | __HOST__: `'${host}'`, 44 | __PORT__: port, 45 | 'process.env': { 46 | NODE_ENV: JSON.stringify('development') 47 | } 48 | }) 49 | ], 50 | resolve: { 51 | extensions: ['', '.js'] 52 | }, 53 | module: { 54 | loaders: [{ 55 | test: /\.js$/, 56 | loader: 'babel', 57 | exclude: /node_modules/, 58 | query: { 59 | presets: ['react-hmre'] 60 | } 61 | }, { 62 | test: /\.css$/, 63 | loaders: [ 64 | 'style', 65 | 'css?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 66 | 'postcss' 67 | ] 68 | }, { 69 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 70 | loader: 'url-loader?limit=100000' 71 | }] 72 | } 73 | }); 74 | 75 | const handleLiveChangesConfig = baseDevConfig(); 76 | handleLiveChangesConfig.entry = [ 77 | customPath, 78 | path.join(__dirname, '../chrome/extension/handleLiveChanges') 79 | ]; 80 | delete handleLiveChangesConfig.hotMiddleware; 81 | delete handleLiveChangesConfig.module.loaders[0].query; 82 | handleLiveChangesConfig.plugins.shift(); // remove HotModuleReplacementPlugin 83 | handleLiveChangesConfig.output = { 84 | path: path.join(__dirname, config.outputDir), 85 | filename: 'handleLiveChanges.bundle.js', 86 | }; 87 | const appConfig = baseDevConfig(); 88 | 89 | return [ 90 | handleLiveChangesConfig, 91 | appConfig 92 | ]; 93 | }; 94 | 95 | module.exports = createDevConfig; 96 | 97 | -------------------------------------------------------------------------------- /webpack/dev.config.js: -------------------------------------------------------------------------------- 1 | const createDevConfig = require('./dev.config.base'); 2 | 3 | module.exports = createDevConfig('chrome'); 4 | -------------------------------------------------------------------------------- /webpack/devFirefox.config.js: -------------------------------------------------------------------------------- 1 | const createDevConfig = require('./dev.config.base'); 2 | 3 | module.exports = createDevConfig('firefox'); 4 | -------------------------------------------------------------------------------- /webpack/prod.config.base.js: -------------------------------------------------------------------------------- 1 | /* eslint import/no-extraneous-dependencies: 0 */ 2 | const path = require('path'); 3 | const webpack = require('webpack'); 4 | 5 | const customPath = path.join(__dirname, './customPublicPath'); 6 | 7 | module.exports = (platform) => { 8 | const config = {}; 9 | if (platform === 'firefox') { 10 | config.outputDir = '../buildFirefox/js'; 11 | } else { 12 | config.outputDir = '../build/js'; 13 | } 14 | 15 | return { 16 | entry: { 17 | popup: [customPath, path.join(__dirname, '../chrome/extension/popup')], 18 | background: [customPath, path.join(__dirname, '../chrome/extension/background')], 19 | handleLiveChanges: [customPath, path.join(__dirname, '../chrome/extension/handleLiveChanges')] 20 | }, 21 | output: { 22 | path: path.join(__dirname, config.outputDir), 23 | filename: '[name].bundle.js', 24 | chunkFilename: '[id].chunk.js' 25 | }, 26 | plugins: [ 27 | new webpack.optimize.OccurenceOrderPlugin(), 28 | new webpack.IgnorePlugin(/[^/]+\/[\S]+.dev$/), 29 | new webpack.optimize.DedupePlugin(), 30 | new webpack.optimize.UglifyJsPlugin({ 31 | comments: false, 32 | compressor: { 33 | warnings: false 34 | } 35 | }), 36 | new webpack.DefinePlugin({ 37 | 'process.env': { 38 | NODE_ENV: JSON.stringify('production') 39 | } 40 | }) 41 | ], 42 | resolve: { 43 | extensions: ['', '.js'] 44 | }, 45 | module: { 46 | loaders: [{ 47 | test: /\.js$/, 48 | loader: 'babel', 49 | exclude: /node_modules/ 50 | }, { 51 | test: /\.css$/, 52 | loaders: [ 53 | 'style', 54 | 'css?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 55 | 'postcss' 56 | ] 57 | }, { 58 | test: /\.(png|woff|woff2|eot|ttf|svg)$/, 59 | loader: 'url-loader?limit=100000' 60 | }] 61 | } 62 | }; 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /webpack/prod.config.js: -------------------------------------------------------------------------------- 1 | const createDevConfig = require('./prod.config.base'); 2 | 3 | module.exports = createDevConfig('chrome'); 4 | -------------------------------------------------------------------------------- /webpack/prodFirefox.config.js: -------------------------------------------------------------------------------- 1 | const createDevConfig = require('./prod.config.base'); 2 | 3 | module.exports = createDevConfig('firefox'); 4 | -------------------------------------------------------------------------------- /webpack/replace/JsonpMainTemplate.runtime.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | /*globals hotAddUpdateChunk parentHotUpdateCallback document XMLHttpRequest $require$ $hotChunkFilename$ $hotMainFilename$ */ 6 | module.exports = function() { 7 | function webpackHotUpdateCallback(chunkId, moreModules) { // eslint-disable-line no-unused-vars 8 | hotAddUpdateChunk(chunkId, moreModules); 9 | if(parentHotUpdateCallback) parentHotUpdateCallback(chunkId, moreModules); 10 | } 11 | 12 | var context = this; 13 | function evalCode(code, context) { 14 | return (function() { return eval(code); }).call(context); 15 | } 16 | 17 | context.hotDownloadUpdateChunk = function (chunkId) { // eslint-disable-line no-unused-vars 18 | var src = __webpack_require__.p + "" + chunkId + "." + hotCurrentHash + ".hot-update.js"; 19 | var request = new XMLHttpRequest(); 20 | 21 | request.onload = function() { 22 | evalCode(this.responseText, context); 23 | }; 24 | request.open("get", src, true); 25 | request.send(); 26 | } 27 | 28 | function hotDownloadManifest(callback) { // eslint-disable-line no-unused-vars 29 | if(typeof XMLHttpRequest === "undefined") 30 | return callback(new Error("No browser support")); 31 | try { 32 | var request = new XMLHttpRequest(); 33 | var requestPath = $require$.p + $hotMainFilename$; 34 | request.open("GET", requestPath, true); 35 | request.timeout = 10000; 36 | request.send(null); 37 | } catch(err) { 38 | return callback(err); 39 | } 40 | request.onreadystatechange = function() { 41 | if(request.readyState !== 4) return; 42 | if(request.status === 0) { 43 | // timeout 44 | callback(new Error("Manifest request to " + requestPath + " timed out.")); 45 | } else if(request.status === 404) { 46 | // no update available 47 | callback(); 48 | } else if(request.status !== 200 && request.status !== 304) { 49 | // other failure 50 | callback(new Error("Manifest request to " + requestPath + " failed.")); 51 | } else { 52 | // success 53 | try { 54 | var update = JSON.parse(request.responseText); 55 | } catch(e) { 56 | callback(e); 57 | return; 58 | } 59 | callback(null, update); 60 | } 61 | }; 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /webpack/replace/log-apply-result.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | module.exports = function(updatedModules, renewedModules) { 6 | var unacceptedModules = updatedModules.filter(function(moduleId) { 7 | return renewedModules && renewedModules.indexOf(moduleId) < 0; 8 | }); 9 | 10 | if(unacceptedModules.length > 0) { 11 | console.warn("[HMR] The following modules couldn't be hot updated: (They would need a full reload!)"); 12 | unacceptedModules.forEach(function(moduleId) { 13 | console.warn("[HMR] - " + moduleId); 14 | }); 15 | 16 | if(chrome && chrome.runtime && chrome.runtime.reload) { 17 | console.warn("[HMR] extension reload"); 18 | chrome.runtime.reload(); 19 | } else { 20 | console.warn("[HMR] Can't extension reload. not found chrome.runtime.reload."); 21 | } 22 | } 23 | 24 | if(!renewedModules || renewedModules.length === 0) { 25 | console.log("[HMR] Nothing hot updated."); 26 | } else { 27 | console.log("[HMR] Updated modules:"); 28 | renewedModules.forEach(function(moduleId) { 29 | console.log("[HMR] - " + moduleId); 30 | }); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /webpack/test.config.js: -------------------------------------------------------------------------------- 1 | // for babel-plugin-webpack-loaders 2 | const config = require('./prod.config'); 3 | 4 | module.exports = { 5 | output: { 6 | libraryTarget: 'commonjs2' 7 | }, 8 | module: { 9 | loaders: config.module.loaders.slice(1) // remove babel-loader 10 | } 11 | }; 12 | --------------------------------------------------------------------------------