├── Archive.zip ├── Pronounce 2.3.zip ├── Pronounce2.5.1.zip ├── README.md ├── browser-polyfill.js ├── icons ├── .DS_Store └── border-48.png ├── index.html ├── manifest.json ├── newtab.html ├── newtab.js ├── options.html ├── options.js ├── package-lock.json └── pronounce.js /Archive.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryhardfifi/pronounce/f545675f417da0932a5bae15a0396b0e98d40f7c/Archive.zip -------------------------------------------------------------------------------- /Pronounce 2.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryhardfifi/pronounce/f545675f417da0932a5bae15a0396b0e98d40f7c/Pronounce 2.3.zip -------------------------------------------------------------------------------- /Pronounce2.5.1.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryhardfifi/pronounce/f545675f417da0932a5bae15a0396b0e98d40f7c/Pronounce2.5.1.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Double-click pronounce: improve your pronounciation for any language 🗣 2 | 3 | Never doubt how to pronounce a word. Double-click it and your browser will read it out loud for you! (Works on Firefox and Chrome). 4 | 5 | Some people use double click to select a word and to then expand the selection to other words, as it would be terrible for you to listen to any selection out loud, double click pronounce will only read out loud when you double-click a single word. 6 | 7 | ## Features 8 | - ✅ Works on Firefox 9 | - ✅ Works on Chrome 10 | - ✅ Language locale selection 11 | - ✅ Toggle on/off 12 | - ✅ Blacklist to turn off in certain webpages 13 | 14 |
Click here to see all locales supported: 15 |

16 | 17 | 18 | - "en-US" 19 | - "it-IT" 20 | - "sv-SE" 21 | - "fr-CA" 22 | - "de-DE" 23 | - "he-IL" 24 | - "id-ID" 25 | - "en-GB" 26 | - "es-AR" 27 | - "nl-BE" 28 | - "en-scotland" 29 | - "en-US" 30 | - "ro-RO" 31 | - "pt-PT" 32 | - "es-ES" 33 | - "es-MX" 34 | - "th-TH" 35 | - "en-AU" 36 | - "ja-JP" 37 | - "sk-SK" 38 | - "hi-IN" 39 | - "it-IT" 40 | - "pt-BR" 41 | - "ar-SA" 42 | - "hu-HU" 43 | - "zh-TW" 44 | - "el-GR" 45 | - "ru-RU" 46 | - "en-IE" 47 | - "es-ES" 48 | - "nb-NO" 49 | - "es-MX" 50 | - "en-IN" 51 | - "en-US" 52 | - "da-DK" 53 | - "fi-FI" 54 | - "zh-HK" 55 | - "en-ZA" 56 | - "fr-FR" 57 | - "zh-CN" 58 | - "en-IN" 59 | - "en-US" 60 | - "nl-NL" 61 | - "tr-TR" 62 | - "ko-KR" 63 | - "ru-RU" 64 | - "pl-PL" 65 | - "cs-CZ" 66 | 67 |

68 |
69 | 70 | 71 | 72 | ## Motivation 73 | All throughout middle school and high school I was a pretty good english student, but ever since I started college I began to lose confidence over my pronounciation (as I didn't have english lessons or anyone to talk to in english). I find this really frustrating because 99.9% of my time online is me reading in english... So the goal of this extension is to practice my pronounciation english. 74 | 75 | This repo was featured in the 138th issue of Ruan YiFeng's blog: https://ruanyifeng.com/blog/2020/12/weekly-issue-138.html 76 | 77 | ## Installation 78 | 79 | ### Firefox 80 | 81 | You can download the extension [here](https://addons.mozilla.org/en-US/firefox/addon/double-click-pronounce/). 82 | 83 | ### Chrome 84 | You can download the extension [here](https://chrome.google.com/webstore/detail/double-click-pronounce/pohphjaomaaadphonldcdmfflfjhnlgf?hl=en). 85 | 86 | ## Contributing 87 | 88 | Feel free to Contribute to this project! 89 | 90 | ## Contributors 91 | 92 | - [@filipeisho](https://github.com/filipeisho) 93 | - [@guimcaballero](https://github.com/guimcaballero) 94 | - [@boncom99](https://github.com/boncom99) 95 | 96 | ## License 97 | 98 | The MIT License (MIT). 99 | -------------------------------------------------------------------------------- /browser-polyfill.js: -------------------------------------------------------------------------------- 1 | 2 | (function (global, factory) { 3 | if (typeof define === "function" && define.amd) { 4 | define("webextension-polyfill", ["module"], factory); 5 | } else if (typeof exports !== "undefined") { 6 | factory(module); 7 | } else { 8 | var mod = { 9 | exports: {} 10 | }; 11 | factory(mod); 12 | global.browser = mod.exports; 13 | } 14 | })(typeof globalThis !== "undefined" ? globalThis : typeof self !== "undefined" ? self : this, function (module) { 15 | /* webextension-polyfill - v0.6.0 - Mon Dec 23 2019 12:32:53 */ 16 | 17 | /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */ 18 | 19 | /* vim: set sts=2 sw=2 et tw=80: */ 20 | 21 | /* This Source Code Form is subject to the terms of the Mozilla Public 22 | * License, v. 2.0. If a copy of the MPL was not distributed with this 23 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 24 | "use strict"; 25 | 26 | if (typeof browser === "undefined" || Object.getPrototypeOf(browser) !== Object.prototype) { 27 | const CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE = "The message port closed before a response was received."; 28 | const SEND_RESPONSE_DEPRECATION_WARNING = "Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)"; // Wrapping the bulk of this polyfill in a one-time-use function is a minor 29 | // optimization for Firefox. Since Spidermonkey does not fully parse the 30 | // contents of a function until the first time it's called, and since it will 31 | // never actually need to be called, this allows the polyfill to be included 32 | // in Firefox nearly for free. 33 | 34 | const wrapAPIs = extensionAPIs => { 35 | // NOTE: apiMetadata is associated to the content of the api-metadata.json file 36 | // at build time by replacing the following "include" with the content of the 37 | // JSON file. 38 | const apiMetadata = { 39 | "alarms": { 40 | "clear": { 41 | "minArgs": 0, 42 | "maxArgs": 1 43 | }, 44 | "clearAll": { 45 | "minArgs": 0, 46 | "maxArgs": 0 47 | }, 48 | "get": { 49 | "minArgs": 0, 50 | "maxArgs": 1 51 | }, 52 | "getAll": { 53 | "minArgs": 0, 54 | "maxArgs": 0 55 | } 56 | }, 57 | "bookmarks": { 58 | "create": { 59 | "minArgs": 1, 60 | "maxArgs": 1 61 | }, 62 | "get": { 63 | "minArgs": 1, 64 | "maxArgs": 1 65 | }, 66 | "getChildren": { 67 | "minArgs": 1, 68 | "maxArgs": 1 69 | }, 70 | "getRecent": { 71 | "minArgs": 1, 72 | "maxArgs": 1 73 | }, 74 | "getSubTree": { 75 | "minArgs": 1, 76 | "maxArgs": 1 77 | }, 78 | "getTree": { 79 | "minArgs": 0, 80 | "maxArgs": 0 81 | }, 82 | "move": { 83 | "minArgs": 2, 84 | "maxArgs": 2 85 | }, 86 | "remove": { 87 | "minArgs": 1, 88 | "maxArgs": 1 89 | }, 90 | "removeTree": { 91 | "minArgs": 1, 92 | "maxArgs": 1 93 | }, 94 | "search": { 95 | "minArgs": 1, 96 | "maxArgs": 1 97 | }, 98 | "update": { 99 | "minArgs": 2, 100 | "maxArgs": 2 101 | } 102 | }, 103 | "browserAction": { 104 | "disable": { 105 | "minArgs": 0, 106 | "maxArgs": 1, 107 | "fallbackToNoCallback": true 108 | }, 109 | "enable": { 110 | "minArgs": 0, 111 | "maxArgs": 1, 112 | "fallbackToNoCallback": true 113 | }, 114 | "getBadgeBackgroundColor": { 115 | "minArgs": 1, 116 | "maxArgs": 1 117 | }, 118 | "getBadgeText": { 119 | "minArgs": 1, 120 | "maxArgs": 1 121 | }, 122 | "getPopup": { 123 | "minArgs": 1, 124 | "maxArgs": 1 125 | }, 126 | "getTitle": { 127 | "minArgs": 1, 128 | "maxArgs": 1 129 | }, 130 | "openPopup": { 131 | "minArgs": 0, 132 | "maxArgs": 0 133 | }, 134 | "setBadgeBackgroundColor": { 135 | "minArgs": 1, 136 | "maxArgs": 1, 137 | "fallbackToNoCallback": true 138 | }, 139 | "setBadgeText": { 140 | "minArgs": 1, 141 | "maxArgs": 1, 142 | "fallbackToNoCallback": true 143 | }, 144 | "setIcon": { 145 | "minArgs": 1, 146 | "maxArgs": 1 147 | }, 148 | "setPopup": { 149 | "minArgs": 1, 150 | "maxArgs": 1, 151 | "fallbackToNoCallback": true 152 | }, 153 | "setTitle": { 154 | "minArgs": 1, 155 | "maxArgs": 1, 156 | "fallbackToNoCallback": true 157 | } 158 | }, 159 | "browsingData": { 160 | "remove": { 161 | "minArgs": 2, 162 | "maxArgs": 2 163 | }, 164 | "removeCache": { 165 | "minArgs": 1, 166 | "maxArgs": 1 167 | }, 168 | "removeCookies": { 169 | "minArgs": 1, 170 | "maxArgs": 1 171 | }, 172 | "removeDownloads": { 173 | "minArgs": 1, 174 | "maxArgs": 1 175 | }, 176 | "removeFormData": { 177 | "minArgs": 1, 178 | "maxArgs": 1 179 | }, 180 | "removeHistory": { 181 | "minArgs": 1, 182 | "maxArgs": 1 183 | }, 184 | "removeLocalStorage": { 185 | "minArgs": 1, 186 | "maxArgs": 1 187 | }, 188 | "removePasswords": { 189 | "minArgs": 1, 190 | "maxArgs": 1 191 | }, 192 | "removePluginData": { 193 | "minArgs": 1, 194 | "maxArgs": 1 195 | }, 196 | "settings": { 197 | "minArgs": 0, 198 | "maxArgs": 0 199 | } 200 | }, 201 | "commands": { 202 | "getAll": { 203 | "minArgs": 0, 204 | "maxArgs": 0 205 | } 206 | }, 207 | "contextMenus": { 208 | "remove": { 209 | "minArgs": 1, 210 | "maxArgs": 1 211 | }, 212 | "removeAll": { 213 | "minArgs": 0, 214 | "maxArgs": 0 215 | }, 216 | "update": { 217 | "minArgs": 2, 218 | "maxArgs": 2 219 | } 220 | }, 221 | "cookies": { 222 | "get": { 223 | "minArgs": 1, 224 | "maxArgs": 1 225 | }, 226 | "getAll": { 227 | "minArgs": 1, 228 | "maxArgs": 1 229 | }, 230 | "getAllCookieStores": { 231 | "minArgs": 0, 232 | "maxArgs": 0 233 | }, 234 | "remove": { 235 | "minArgs": 1, 236 | "maxArgs": 1 237 | }, 238 | "set": { 239 | "minArgs": 1, 240 | "maxArgs": 1 241 | } 242 | }, 243 | "devtools": { 244 | "inspectedWindow": { 245 | "eval": { 246 | "minArgs": 1, 247 | "maxArgs": 2, 248 | "singleCallbackArg": false 249 | } 250 | }, 251 | "panels": { 252 | "create": { 253 | "minArgs": 3, 254 | "maxArgs": 3, 255 | "singleCallbackArg": true 256 | } 257 | } 258 | }, 259 | "downloads": { 260 | "cancel": { 261 | "minArgs": 1, 262 | "maxArgs": 1 263 | }, 264 | "download": { 265 | "minArgs": 1, 266 | "maxArgs": 1 267 | }, 268 | "erase": { 269 | "minArgs": 1, 270 | "maxArgs": 1 271 | }, 272 | "getFileIcon": { 273 | "minArgs": 1, 274 | "maxArgs": 2 275 | }, 276 | "open": { 277 | "minArgs": 1, 278 | "maxArgs": 1, 279 | "fallbackToNoCallback": true 280 | }, 281 | "pause": { 282 | "minArgs": 1, 283 | "maxArgs": 1 284 | }, 285 | "removeFile": { 286 | "minArgs": 1, 287 | "maxArgs": 1 288 | }, 289 | "resume": { 290 | "minArgs": 1, 291 | "maxArgs": 1 292 | }, 293 | "search": { 294 | "minArgs": 1, 295 | "maxArgs": 1 296 | }, 297 | "show": { 298 | "minArgs": 1, 299 | "maxArgs": 1, 300 | "fallbackToNoCallback": true 301 | } 302 | }, 303 | "extension": { 304 | "isAllowedFileSchemeAccess": { 305 | "minArgs": 0, 306 | "maxArgs": 0 307 | }, 308 | "isAllowedIncognitoAccess": { 309 | "minArgs": 0, 310 | "maxArgs": 0 311 | } 312 | }, 313 | "history": { 314 | "addUrl": { 315 | "minArgs": 1, 316 | "maxArgs": 1 317 | }, 318 | "deleteAll": { 319 | "minArgs": 0, 320 | "maxArgs": 0 321 | }, 322 | "deleteRange": { 323 | "minArgs": 1, 324 | "maxArgs": 1 325 | }, 326 | "deleteUrl": { 327 | "minArgs": 1, 328 | "maxArgs": 1 329 | }, 330 | "getVisits": { 331 | "minArgs": 1, 332 | "maxArgs": 1 333 | }, 334 | "search": { 335 | "minArgs": 1, 336 | "maxArgs": 1 337 | } 338 | }, 339 | "i18n": { 340 | "detectLanguage": { 341 | "minArgs": 1, 342 | "maxArgs": 1 343 | }, 344 | "getAcceptLanguages": { 345 | "minArgs": 0, 346 | "maxArgs": 0 347 | } 348 | }, 349 | "identity": { 350 | "launchWebAuthFlow": { 351 | "minArgs": 1, 352 | "maxArgs": 1 353 | } 354 | }, 355 | "idle": { 356 | "queryState": { 357 | "minArgs": 1, 358 | "maxArgs": 1 359 | } 360 | }, 361 | "management": { 362 | "get": { 363 | "minArgs": 1, 364 | "maxArgs": 1 365 | }, 366 | "getAll": { 367 | "minArgs": 0, 368 | "maxArgs": 0 369 | }, 370 | "getSelf": { 371 | "minArgs": 0, 372 | "maxArgs": 0 373 | }, 374 | "setEnabled": { 375 | "minArgs": 2, 376 | "maxArgs": 2 377 | }, 378 | "uninstallSelf": { 379 | "minArgs": 0, 380 | "maxArgs": 1 381 | } 382 | }, 383 | "notifications": { 384 | "clear": { 385 | "minArgs": 1, 386 | "maxArgs": 1 387 | }, 388 | "create": { 389 | "minArgs": 1, 390 | "maxArgs": 2 391 | }, 392 | "getAll": { 393 | "minArgs": 0, 394 | "maxArgs": 0 395 | }, 396 | "getPermissionLevel": { 397 | "minArgs": 0, 398 | "maxArgs": 0 399 | }, 400 | "update": { 401 | "minArgs": 2, 402 | "maxArgs": 2 403 | } 404 | }, 405 | "pageAction": { 406 | "getPopup": { 407 | "minArgs": 1, 408 | "maxArgs": 1 409 | }, 410 | "getTitle": { 411 | "minArgs": 1, 412 | "maxArgs": 1 413 | }, 414 | "hide": { 415 | "minArgs": 1, 416 | "maxArgs": 1, 417 | "fallbackToNoCallback": true 418 | }, 419 | "setIcon": { 420 | "minArgs": 1, 421 | "maxArgs": 1 422 | }, 423 | "setPopup": { 424 | "minArgs": 1, 425 | "maxArgs": 1, 426 | "fallbackToNoCallback": true 427 | }, 428 | "setTitle": { 429 | "minArgs": 1, 430 | "maxArgs": 1, 431 | "fallbackToNoCallback": true 432 | }, 433 | "show": { 434 | "minArgs": 1, 435 | "maxArgs": 1, 436 | "fallbackToNoCallback": true 437 | } 438 | }, 439 | "permissions": { 440 | "contains": { 441 | "minArgs": 1, 442 | "maxArgs": 1 443 | }, 444 | "getAll": { 445 | "minArgs": 0, 446 | "maxArgs": 0 447 | }, 448 | "remove": { 449 | "minArgs": 1, 450 | "maxArgs": 1 451 | }, 452 | "request": { 453 | "minArgs": 1, 454 | "maxArgs": 1 455 | } 456 | }, 457 | "runtime": { 458 | "getBackgroundPage": { 459 | "minArgs": 0, 460 | "maxArgs": 0 461 | }, 462 | "getPlatformInfo": { 463 | "minArgs": 0, 464 | "maxArgs": 0 465 | }, 466 | "openOptionsPage": { 467 | "minArgs": 0, 468 | "maxArgs": 0 469 | }, 470 | "requestUpdateCheck": { 471 | "minArgs": 0, 472 | "maxArgs": 0 473 | }, 474 | "sendMessage": { 475 | "minArgs": 1, 476 | "maxArgs": 3 477 | }, 478 | "sendNativeMessage": { 479 | "minArgs": 2, 480 | "maxArgs": 2 481 | }, 482 | "setUninstallURL": { 483 | "minArgs": 1, 484 | "maxArgs": 1 485 | } 486 | }, 487 | "sessions": { 488 | "getDevices": { 489 | "minArgs": 0, 490 | "maxArgs": 1 491 | }, 492 | "getRecentlyClosed": { 493 | "minArgs": 0, 494 | "maxArgs": 1 495 | }, 496 | "restore": { 497 | "minArgs": 0, 498 | "maxArgs": 1 499 | } 500 | }, 501 | "storage": { 502 | "local": { 503 | "clear": { 504 | "minArgs": 0, 505 | "maxArgs": 0 506 | }, 507 | "get": { 508 | "minArgs": 0, 509 | "maxArgs": 1 510 | }, 511 | "getBytesInUse": { 512 | "minArgs": 0, 513 | "maxArgs": 1 514 | }, 515 | "remove": { 516 | "minArgs": 1, 517 | "maxArgs": 1 518 | }, 519 | "set": { 520 | "minArgs": 1, 521 | "maxArgs": 1 522 | } 523 | }, 524 | "managed": { 525 | "get": { 526 | "minArgs": 0, 527 | "maxArgs": 1 528 | }, 529 | "getBytesInUse": { 530 | "minArgs": 0, 531 | "maxArgs": 1 532 | } 533 | }, 534 | "sync": { 535 | "clear": { 536 | "minArgs": 0, 537 | "maxArgs": 0 538 | }, 539 | "get": { 540 | "minArgs": 0, 541 | "maxArgs": 1 542 | }, 543 | "getBytesInUse": { 544 | "minArgs": 0, 545 | "maxArgs": 1 546 | }, 547 | "remove": { 548 | "minArgs": 1, 549 | "maxArgs": 1 550 | }, 551 | "set": { 552 | "minArgs": 1, 553 | "maxArgs": 1 554 | } 555 | } 556 | }, 557 | "tabs": { 558 | "captureVisibleTab": { 559 | "minArgs": 0, 560 | "maxArgs": 2 561 | }, 562 | "create": { 563 | "minArgs": 1, 564 | "maxArgs": 1 565 | }, 566 | "detectLanguage": { 567 | "minArgs": 0, 568 | "maxArgs": 1 569 | }, 570 | "discard": { 571 | "minArgs": 0, 572 | "maxArgs": 1 573 | }, 574 | "duplicate": { 575 | "minArgs": 1, 576 | "maxArgs": 1 577 | }, 578 | "executeScript": { 579 | "minArgs": 1, 580 | "maxArgs": 2 581 | }, 582 | "get": { 583 | "minArgs": 1, 584 | "maxArgs": 1 585 | }, 586 | "getCurrent": { 587 | "minArgs": 0, 588 | "maxArgs": 0 589 | }, 590 | "getZoom": { 591 | "minArgs": 0, 592 | "maxArgs": 1 593 | }, 594 | "getZoomSettings": { 595 | "minArgs": 0, 596 | "maxArgs": 1 597 | }, 598 | "highlight": { 599 | "minArgs": 1, 600 | "maxArgs": 1 601 | }, 602 | "insertCSS": { 603 | "minArgs": 1, 604 | "maxArgs": 2 605 | }, 606 | "move": { 607 | "minArgs": 2, 608 | "maxArgs": 2 609 | }, 610 | "query": { 611 | "minArgs": 1, 612 | "maxArgs": 1 613 | }, 614 | "reload": { 615 | "minArgs": 0, 616 | "maxArgs": 2 617 | }, 618 | "remove": { 619 | "minArgs": 1, 620 | "maxArgs": 1 621 | }, 622 | "removeCSS": { 623 | "minArgs": 1, 624 | "maxArgs": 2 625 | }, 626 | "sendMessage": { 627 | "minArgs": 2, 628 | "maxArgs": 3 629 | }, 630 | "setZoom": { 631 | "minArgs": 1, 632 | "maxArgs": 2 633 | }, 634 | "setZoomSettings": { 635 | "minArgs": 1, 636 | "maxArgs": 2 637 | }, 638 | "update": { 639 | "minArgs": 1, 640 | "maxArgs": 2 641 | } 642 | }, 643 | "topSites": { 644 | "get": { 645 | "minArgs": 0, 646 | "maxArgs": 0 647 | } 648 | }, 649 | "webNavigation": { 650 | "getAllFrames": { 651 | "minArgs": 1, 652 | "maxArgs": 1 653 | }, 654 | "getFrame": { 655 | "minArgs": 1, 656 | "maxArgs": 1 657 | } 658 | }, 659 | "webRequest": { 660 | "handlerBehaviorChanged": { 661 | "minArgs": 0, 662 | "maxArgs": 0 663 | } 664 | }, 665 | "windows": { 666 | "create": { 667 | "minArgs": 0, 668 | "maxArgs": 1 669 | }, 670 | "get": { 671 | "minArgs": 1, 672 | "maxArgs": 2 673 | }, 674 | "getAll": { 675 | "minArgs": 0, 676 | "maxArgs": 1 677 | }, 678 | "getCurrent": { 679 | "minArgs": 0, 680 | "maxArgs": 1 681 | }, 682 | "getLastFocused": { 683 | "minArgs": 0, 684 | "maxArgs": 1 685 | }, 686 | "remove": { 687 | "minArgs": 1, 688 | "maxArgs": 1 689 | }, 690 | "update": { 691 | "minArgs": 2, 692 | "maxArgs": 2 693 | } 694 | } 695 | }; 696 | 697 | if (Object.keys(apiMetadata).length === 0) { 698 | throw new Error("api-metadata.json has not been included in browser-polyfill"); 699 | } 700 | /** 701 | * A WeakMap subclass which creates and stores a value for any key which does 702 | * not exist when accessed, but behaves exactly as an ordinary WeakMap 703 | * otherwise. 704 | * 705 | * @param {function} createItem 706 | * A function which will be called in order to create the value for any 707 | * key which does not exist, the first time it is accessed. The 708 | * function receives, as its only argument, the key being created. 709 | */ 710 | 711 | 712 | class DefaultWeakMap extends WeakMap { 713 | constructor(createItem, items = undefined) { 714 | super(items); 715 | this.createItem = createItem; 716 | } 717 | 718 | get(key) { 719 | if (!this.has(key)) { 720 | this.set(key, this.createItem(key)); 721 | } 722 | 723 | return super.get(key); 724 | } 725 | 726 | } 727 | /** 728 | * Returns true if the given object is an object with a `then` method, and can 729 | * therefore be assumed to behave as a Promise. 730 | * 731 | * @param {*} value The value to test. 732 | * @returns {boolean} True if the value is thenable. 733 | */ 734 | 735 | 736 | const isThenable = value => { 737 | return value && typeof value === "object" && typeof value.then === "function"; 738 | }; 739 | /** 740 | * Creates and returns a function which, when called, will resolve or reject 741 | * the given promise based on how it is called: 742 | * 743 | * - If, when called, `chrome.runtime.lastError` contains a non-null object, 744 | * the promise is rejected with that value. 745 | * - If the function is called with exactly one argument, the promise is 746 | * resolved to that value. 747 | * - Otherwise, the promise is resolved to an array containing all of the 748 | * function's arguments. 749 | * 750 | * @param {object} promise 751 | * An object containing the resolution and rejection functions of a 752 | * promise. 753 | * @param {function} promise.resolve 754 | * The promise's resolution function. 755 | * @param {function} promise.rejection 756 | * The promise's rejection function. 757 | * @param {object} metadata 758 | * Metadata about the wrapped method which has created the callback. 759 | * @param {integer} metadata.maxResolvedArgs 760 | * The maximum number of arguments which may be passed to the 761 | * callback created by the wrapped async function. 762 | * 763 | * @returns {function} 764 | * The generated callback function. 765 | */ 766 | 767 | 768 | const makeCallback = (promise, metadata) => { 769 | return (...callbackArgs) => { 770 | if (extensionAPIs.runtime.lastError) { 771 | promise.reject(extensionAPIs.runtime.lastError); 772 | } else if (metadata.singleCallbackArg || callbackArgs.length <= 1 && metadata.singleCallbackArg !== false) { 773 | promise.resolve(callbackArgs[0]); 774 | } else { 775 | promise.resolve(callbackArgs); 776 | } 777 | }; 778 | }; 779 | 780 | const pluralizeArguments = numArgs => numArgs == 1 ? "argument" : "arguments"; 781 | /** 782 | * Creates a wrapper function for a method with the given name and metadata. 783 | * 784 | * @param {string} name 785 | * The name of the method which is being wrapped. 786 | * @param {object} metadata 787 | * Metadata about the method being wrapped. 788 | * @param {integer} metadata.minArgs 789 | * The minimum number of arguments which must be passed to the 790 | * function. If called with fewer than this number of arguments, the 791 | * wrapper will raise an exception. 792 | * @param {integer} metadata.maxArgs 793 | * The maximum number of arguments which may be passed to the 794 | * function. If called with more than this number of arguments, the 795 | * wrapper will raise an exception. 796 | * @param {integer} metadata.maxResolvedArgs 797 | * The maximum number of arguments which may be passed to the 798 | * callback created by the wrapped async function. 799 | * 800 | * @returns {function(object, ...*)} 801 | * The generated wrapper function. 802 | */ 803 | 804 | 805 | const wrapAsyncFunction = (name, metadata) => { 806 | return function asyncFunctionWrapper(target, ...args) { 807 | if (args.length < metadata.minArgs) { 808 | throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); 809 | } 810 | 811 | if (args.length > metadata.maxArgs) { 812 | throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); 813 | } 814 | 815 | return new Promise((resolve, reject) => { 816 | if (metadata.fallbackToNoCallback) { 817 | // This API method has currently no callback on Chrome, but it return a promise on Firefox, 818 | // and so the polyfill will try to call it with a callback first, and it will fallback 819 | // to not passing the callback if the first call fails. 820 | try { 821 | target[name](...args, makeCallback({ 822 | resolve, 823 | reject 824 | }, metadata)); 825 | } catch (cbError) { 826 | console.warn(`${name} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", cbError); 827 | target[name](...args); // Update the API method metadata, so that the next API calls will not try to 828 | // use the unsupported callback anymore. 829 | 830 | metadata.fallbackToNoCallback = false; 831 | metadata.noCallback = true; 832 | resolve(); 833 | } 834 | } else if (metadata.noCallback) { 835 | target[name](...args); 836 | resolve(); 837 | } else { 838 | target[name](...args, makeCallback({ 839 | resolve, 840 | reject 841 | }, metadata)); 842 | } 843 | }); 844 | }; 845 | }; 846 | /** 847 | * Wraps an existing method of the target object, so that calls to it are 848 | * intercepted by the given wrapper function. The wrapper function receives, 849 | * as its first argument, the original `target` object, followed by each of 850 | * the arguments passed to the original method. 851 | * 852 | * @param {object} target 853 | * The original target object that the wrapped method belongs to. 854 | * @param {function} method 855 | * The method being wrapped. This is used as the target of the Proxy 856 | * object which is created to wrap the method. 857 | * @param {function} wrapper 858 | * The wrapper function which is called in place of a direct invocation 859 | * of the wrapped method. 860 | * 861 | * @returns {Proxy} 862 | * A Proxy object for the given method, which invokes the given wrapper 863 | * method in its place. 864 | */ 865 | 866 | 867 | const wrapMethod = (target, method, wrapper) => { 868 | return new Proxy(method, { 869 | apply(targetMethod, thisObj, args) { 870 | return wrapper.call(thisObj, target, ...args); 871 | } 872 | 873 | }); 874 | }; 875 | 876 | let hasOwnProperty = Function.call.bind(Object.prototype.hasOwnProperty); 877 | /** 878 | * Wraps an object in a Proxy which intercepts and wraps certain methods 879 | * based on the given `wrappers` and `metadata` objects. 880 | * 881 | * @param {object} target 882 | * The target object to wrap. 883 | * 884 | * @param {object} [wrappers = {}] 885 | * An object tree containing wrapper functions for special cases. Any 886 | * function present in this object tree is called in place of the 887 | * method in the same location in the `target` object tree. These 888 | * wrapper methods are invoked as described in {@see wrapMethod}. 889 | * 890 | * @param {object} [metadata = {}] 891 | * An object tree containing metadata used to automatically generate 892 | * Promise-based wrapper functions for asynchronous. Any function in 893 | * the `target` object tree which has a corresponding metadata object 894 | * in the same location in the `metadata` tree is replaced with an 895 | * automatically-generated wrapper function, as described in 896 | * {@see wrapAsyncFunction} 897 | * 898 | * @returns {Proxy} 899 | */ 900 | 901 | const wrapObject = (target, wrappers = {}, metadata = {}) => { 902 | let cache = Object.create(null); 903 | let handlers = { 904 | has(proxyTarget, prop) { 905 | return prop in target || prop in cache; 906 | }, 907 | 908 | get(proxyTarget, prop, receiver) { 909 | if (prop in cache) { 910 | return cache[prop]; 911 | } 912 | 913 | if (!(prop in target)) { 914 | return undefined; 915 | } 916 | 917 | let value = target[prop]; 918 | 919 | if (typeof value === "function") { 920 | // This is a method on the underlying object. Check if we need to do 921 | // any wrapping. 922 | if (typeof wrappers[prop] === "function") { 923 | // We have a special-case wrapper for this method. 924 | value = wrapMethod(target, target[prop], wrappers[prop]); 925 | } else if (hasOwnProperty(metadata, prop)) { 926 | // This is an async method that we have metadata for. Create a 927 | // Promise wrapper for it. 928 | let wrapper = wrapAsyncFunction(prop, metadata[prop]); 929 | value = wrapMethod(target, target[prop], wrapper); 930 | } else { 931 | // This is a method that we don't know or care about. Return the 932 | // original method, bound to the underlying object. 933 | value = value.bind(target); 934 | } 935 | } else if (typeof value === "object" && value !== null && (hasOwnProperty(wrappers, prop) || hasOwnProperty(metadata, prop))) { 936 | // This is an object that we need to do some wrapping for the children 937 | // of. Create a sub-object wrapper for it with the appropriate child 938 | // metadata. 939 | value = wrapObject(value, wrappers[prop], metadata[prop]); 940 | } else if (hasOwnProperty(metadata, "*")) { 941 | // Wrap all properties in * namespace. 942 | value = wrapObject(value, wrappers[prop], metadata["*"]); 943 | } else { 944 | // We don't need to do any wrapping for this property, 945 | // so just forward all access to the underlying object. 946 | Object.defineProperty(cache, prop, { 947 | configurable: true, 948 | enumerable: true, 949 | 950 | get() { 951 | return target[prop]; 952 | }, 953 | 954 | set(value) { 955 | target[prop] = value; 956 | } 957 | 958 | }); 959 | return value; 960 | } 961 | 962 | cache[prop] = value; 963 | return value; 964 | }, 965 | 966 | set(proxyTarget, prop, value, receiver) { 967 | if (prop in cache) { 968 | cache[prop] = value; 969 | } else { 970 | target[prop] = value; 971 | } 972 | 973 | return true; 974 | }, 975 | 976 | defineProperty(proxyTarget, prop, desc) { 977 | return Reflect.defineProperty(cache, prop, desc); 978 | }, 979 | 980 | deleteProperty(proxyTarget, prop) { 981 | return Reflect.deleteProperty(cache, prop); 982 | } 983 | 984 | }; // Per contract of the Proxy API, the "get" proxy handler must return the 985 | // original value of the target if that value is declared read-only and 986 | // non-configurable. For this reason, we create an object with the 987 | // prototype set to `target` instead of using `target` directly. 988 | // Otherwise we cannot return a custom object for APIs that 989 | // are declared read-only and non-configurable, such as `chrome.devtools`. 990 | // 991 | // The proxy handlers themselves will still use the original `target` 992 | // instead of the `proxyTarget`, so that the methods and properties are 993 | // dereferenced via the original targets. 994 | 995 | let proxyTarget = Object.create(target); 996 | return new Proxy(proxyTarget, handlers); 997 | }; 998 | /** 999 | * Creates a set of wrapper functions for an event object, which handles 1000 | * wrapping of listener functions that those messages are passed. 1001 | * 1002 | * A single wrapper is created for each listener function, and stored in a 1003 | * map. Subsequent calls to `addListener`, `hasListener`, or `removeListener` 1004 | * retrieve the original wrapper, so that attempts to remove a 1005 | * previously-added listener work as expected. 1006 | * 1007 | * @param {DefaultWeakMap} wrapperMap 1008 | * A DefaultWeakMap object which will create the appropriate wrapper 1009 | * for a given listener function when one does not exist, and retrieve 1010 | * an existing one when it does. 1011 | * 1012 | * @returns {object} 1013 | */ 1014 | 1015 | 1016 | const wrapEvent = wrapperMap => ({ 1017 | addListener(target, listener, ...args) { 1018 | target.addListener(wrapperMap.get(listener), ...args); 1019 | }, 1020 | 1021 | hasListener(target, listener) { 1022 | return target.hasListener(wrapperMap.get(listener)); 1023 | }, 1024 | 1025 | removeListener(target, listener) { 1026 | target.removeListener(wrapperMap.get(listener)); 1027 | } 1028 | 1029 | }); // Keep track if the deprecation warning has been logged at least once. 1030 | 1031 | 1032 | let loggedSendResponseDeprecationWarning = false; 1033 | const onMessageWrappers = new DefaultWeakMap(listener => { 1034 | if (typeof listener !== "function") { 1035 | return listener; 1036 | } 1037 | /** 1038 | * Wraps a message listener function so that it may send responses based on 1039 | * its return value, rather than by returning a sentinel value and calling a 1040 | * callback. If the listener function returns a Promise, the response is 1041 | * sent when the promise either resolves or rejects. 1042 | * 1043 | * @param {*} message 1044 | * The message sent by the other end of the channel. 1045 | * @param {object} sender 1046 | * Details about the sender of the message. 1047 | * @param {function(*)} sendResponse 1048 | * A callback which, when called with an arbitrary argument, sends 1049 | * that value as a response. 1050 | * @returns {boolean} 1051 | * True if the wrapped listener returned a Promise, which will later 1052 | * yield a response. False otherwise. 1053 | */ 1054 | 1055 | 1056 | return function onMessage(message, sender, sendResponse) { 1057 | let didCallSendResponse = false; 1058 | let wrappedSendResponse; 1059 | let sendResponsePromise = new Promise(resolve => { 1060 | wrappedSendResponse = function (response) { 1061 | if (!loggedSendResponseDeprecationWarning) { 1062 | console.warn(SEND_RESPONSE_DEPRECATION_WARNING, new Error().stack); 1063 | loggedSendResponseDeprecationWarning = true; 1064 | } 1065 | 1066 | didCallSendResponse = true; 1067 | resolve(response); 1068 | }; 1069 | }); 1070 | let result; 1071 | 1072 | try { 1073 | result = listener(message, sender, wrappedSendResponse); 1074 | } catch (err) { 1075 | result = Promise.reject(err); 1076 | } 1077 | 1078 | const isResultThenable = result !== true && isThenable(result); // If the listener didn't returned true or a Promise, or called 1079 | // wrappedSendResponse synchronously, we can exit earlier 1080 | // because there will be no response sent from this listener. 1081 | 1082 | if (result !== true && !isResultThenable && !didCallSendResponse) { 1083 | return false; 1084 | } // A small helper to send the message if the promise resolves 1085 | // and an error if the promise rejects (a wrapped sendMessage has 1086 | // to translate the message into a resolved promise or a rejected 1087 | // promise). 1088 | 1089 | 1090 | const sendPromisedResult = promise => { 1091 | promise.then(msg => { 1092 | // send the message value. 1093 | sendResponse(msg); 1094 | }, error => { 1095 | // Send a JSON representation of the error if the rejected value 1096 | // is an instance of error, or the object itself otherwise. 1097 | let message; 1098 | 1099 | if (error && (error instanceof Error || typeof error.message === "string")) { 1100 | message = error.message; 1101 | } else { 1102 | message = "An unexpected error occurred"; 1103 | } 1104 | 1105 | sendResponse({ 1106 | __mozWebExtensionPolyfillReject__: true, 1107 | message 1108 | }); 1109 | }).catch(err => { 1110 | // Print an error on the console if unable to send the response. 1111 | console.error("Failed to send onMessage rejected reply", err); 1112 | }); 1113 | }; // If the listener returned a Promise, send the resolved value as a 1114 | // result, otherwise wait the promise related to the wrappedSendResponse 1115 | // callback to resolve and send it as a response. 1116 | 1117 | 1118 | if (isResultThenable) { 1119 | sendPromisedResult(result); 1120 | } else { 1121 | sendPromisedResult(sendResponsePromise); 1122 | } // Let Chrome know that the listener is replying. 1123 | 1124 | 1125 | return true; 1126 | }; 1127 | }); 1128 | 1129 | const wrappedSendMessageCallback = ({ 1130 | reject, 1131 | resolve 1132 | }, reply) => { 1133 | if (extensionAPIs.runtime.lastError) { 1134 | // Detect when none of the listeners replied to the sendMessage call and resolve 1135 | // the promise to undefined as in Firefox. 1136 | // See https://github.com/mozilla/webextension-polyfill/issues/130 1137 | if (extensionAPIs.runtime.lastError.message === CHROME_SEND_MESSAGE_CALLBACK_NO_RESPONSE_MESSAGE) { 1138 | resolve(); 1139 | } else { 1140 | reject(extensionAPIs.runtime.lastError); 1141 | } 1142 | } else if (reply && reply.__mozWebExtensionPolyfillReject__) { 1143 | // Convert back the JSON representation of the error into 1144 | // an Error instance. 1145 | reject(new Error(reply.message)); 1146 | } else { 1147 | resolve(reply); 1148 | } 1149 | }; 1150 | 1151 | const wrappedSendMessage = (name, metadata, apiNamespaceObj, ...args) => { 1152 | if (args.length < metadata.minArgs) { 1153 | throw new Error(`Expected at least ${metadata.minArgs} ${pluralizeArguments(metadata.minArgs)} for ${name}(), got ${args.length}`); 1154 | } 1155 | 1156 | if (args.length > metadata.maxArgs) { 1157 | throw new Error(`Expected at most ${metadata.maxArgs} ${pluralizeArguments(metadata.maxArgs)} for ${name}(), got ${args.length}`); 1158 | } 1159 | 1160 | return new Promise((resolve, reject) => { 1161 | const wrappedCb = wrappedSendMessageCallback.bind(null, { 1162 | resolve, 1163 | reject 1164 | }); 1165 | args.push(wrappedCb); 1166 | apiNamespaceObj.sendMessage(...args); 1167 | }); 1168 | }; 1169 | 1170 | const staticWrappers = { 1171 | runtime: { 1172 | onMessage: wrapEvent(onMessageWrappers), 1173 | onMessageExternal: wrapEvent(onMessageWrappers), 1174 | sendMessage: wrappedSendMessage.bind(null, "sendMessage", { 1175 | minArgs: 1, 1176 | maxArgs: 3 1177 | }) 1178 | }, 1179 | tabs: { 1180 | sendMessage: wrappedSendMessage.bind(null, "sendMessage", { 1181 | minArgs: 2, 1182 | maxArgs: 3 1183 | }) 1184 | } 1185 | }; 1186 | const settingMetadata = { 1187 | clear: { 1188 | minArgs: 1, 1189 | maxArgs: 1 1190 | }, 1191 | get: { 1192 | minArgs: 1, 1193 | maxArgs: 1 1194 | }, 1195 | set: { 1196 | minArgs: 1, 1197 | maxArgs: 1 1198 | } 1199 | }; 1200 | apiMetadata.privacy = { 1201 | network: { 1202 | "*": settingMetadata 1203 | }, 1204 | services: { 1205 | "*": settingMetadata 1206 | }, 1207 | websites: { 1208 | "*": settingMetadata 1209 | } 1210 | }; 1211 | return wrapObject(extensionAPIs, staticWrappers, apiMetadata); 1212 | }; 1213 | 1214 | if (typeof chrome != "object" || !chrome || !chrome.runtime || !chrome.runtime.id) { 1215 | throw new Error("This script should only be loaded in a browser extension."); 1216 | } // The build process adds a UMD wrapper around this file, which makes the 1217 | // `module` variable available. 1218 | 1219 | 1220 | module.exports = wrapAPIs(chrome); 1221 | } else { 1222 | module.exports = browser; 1223 | } 1224 | }); 1225 | //# sourceMappingURL=browser-polyfill.js.map 1226 | -------------------------------------------------------------------------------- /icons/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryhardfifi/pronounce/f545675f417da0932a5bae15a0396b0e98d40f7c/icons/.DS_Store -------------------------------------------------------------------------------- /icons/border-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tryhardfifi/pronounce/f545675f417da0932a5bae15a0396b0e98d40f7c/icons/border-48.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Options will be loaded for all new pages on Chrome. Alternatively reload the page.

14 |
15 | Active 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | "manifest_version": 2, 4 | "name": "Double-click pronounce", 5 | "version": "2.5.1", 6 | 7 | "description": "Never doubt how to pronounce a word. Better your spoken languages!", 8 | 9 | "icons": { 10 | "48": "icons/border-48.png" 11 | }, 12 | 13 | "background": { 14 | "persistent": false, 15 | "scripts": [ 16 | "browser-polyfill.js" 17 | ] 18 | }, 19 | "content_scripts": [ 20 | { 21 | "matches": [""], 22 | "js": ["browser-polyfill.js","pronounce.js"] 23 | } 24 | ], 25 | 26 | "options_ui": { 27 | "page": "options.html" 28 | }, 29 | 30 | "permissions": ["tabs","storage", "management"], 31 | "chrome_url_overrides" : { 32 | "newtab": "newtab.html" 33 | }, 34 | 35 | 36 | "browser_specific_settings": { 37 | "gecko": { 38 | "id": "{f8716d82-2c5c-430d-b783-5473310342cf}" 39 | } 40 | }, 41 | "browser_action": { 42 | "default_icon": { 43 | "32": "icons/border-48.png" 44 | }, 45 | 46 | "default_title": "Pronounce", 47 | "default_popup": "options.html" 48 | } 49 | 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /newtab.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
Pronounce:
12 |
13 |
14 | total words pronounced: 15 | 16 |
17 | 18 | 19 | 83 | 84 | -------------------------------------------------------------------------------- /newtab.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Generate HTML List From JavaScript Array 3 | * 4 | * @uri https://getbutterfly.com/generate-html-list-from-javascript-array/ 5 | */ 6 | function makeList(listData) { 7 | // Establish the array which acts as a data source for the list 8 | console.log(listData); 9 | // Make a container element for the list 10 | listContainer = document.getElementById("1"); 11 | wordContainer = document.getElementById("2"); 12 | counter = document.getElementById("3"); 13 | // Make the list 14 | listElement = document.createElement("ul"); 15 | 16 | document.getElementsByTagName("body")[0].appendChild(wordContainer); 17 | numberOfListItems = listData.length; 18 | counter.innerHTML = numberOfListItems; 19 | var rand = listData[Math.floor(Math.random() * numberOfListItems)]; 20 | wordContainer.innerHTML = rand; 21 | 22 | document.getElementsByTagName("body")[0].appendChild(listContainer); 23 | listContainer.appendChild(listElement); 24 | for (i = 0; i < numberOfListItems; ++i) { 25 | // create an item for each one 26 | listItem = document.createElement("li"); 27 | 28 | // Add the item text 29 | listItem.innerHTML = listData[i]; 30 | 31 | // Add listItem to the listElement 32 | listElement.appendChild(listItem); 33 | } 34 | } 35 | 36 | async function setList(result) { 37 | if (list != null) { 38 | list = JSON.parse(result.list); 39 | } else { 40 | list = []; 41 | } 42 | makeList(list); 43 | } 44 | 45 | function onError(error) {} 46 | let list = browser.storage.sync.get("list"); 47 | list.then(setList, onError); 48 | let a = document.getElementById("my-manifest-placeholder"); 49 | console.log(a); 50 | const stringManifest = JSON.stringify(myDynamicManifest); 51 | const blob = new Blob([stringManifest], { type: "application/json" }); 52 | const manifestURL = URL.createObjectURL(blob); 53 | document 54 | .querySelector("#my-manifest-placeholder") 55 | .setAttribute("href", manifestURL); 56 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Options will be loaded for all new pages on Chrome. Alternatively reload the page.

14 |
15 | Active 16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | async function getPage() {} 2 | function populateVoiceList() { 3 | if (typeof speechSynthesis === "undefined") { 4 | return; 5 | } 6 | 7 | var voices = speechSynthesis.getVoices(); 8 | 9 | for (var i = 0; i < voices.length; i++) { 10 | var option = document.createElement("option"); 11 | option.textContent = voices[i].name + " (" + voices[i].lang + ")"; 12 | 13 | if (voices[i].default) { 14 | option.textContent += " -- DEFAULT"; 15 | } 16 | 17 | option.setAttribute("data-lang", voices[i].lang); 18 | option.setAttribute("data-name", voices[i].name); 19 | document.getElementById("voiceSelect").appendChild(option); 20 | } 21 | } 22 | 23 | populateVoiceList(); 24 | if ( 25 | typeof speechSynthesis !== "undefined" && 26 | speechSynthesis.onvoiceschanged !== undefined 27 | ) { 28 | speechSynthesis.onvoiceschanged = populateVoiceList; 29 | } 30 | function saveOptions(e) { 31 | e.preventDefault(); 32 | browser.storage.sync.set({ 33 | voiceSelect: document.querySelector("#voiceSelect").value, 34 | isOn: document.querySelector("#isOn").checked 35 | }); 36 | 37 | browser.runtime.reload(); 38 | window.close(); 39 | } 40 | 41 | function url_domain(data) { 42 | var a = document.createElement("a"); 43 | a.href = data; 44 | return a.hostname; 45 | } 46 | 47 | function addToBlacklist(e) { 48 | async function store(result) { 49 | 50 | if (result.blacklist == null){ 51 | blacklist = [""]; 52 | } 53 | else{ 54 | blacklist = JSON.parse(result.blacklist); 55 | } 56 | current_url = await browser.tabs 57 | .query({ currentWindow: true, active: true }) 58 | .then((tabs) => { 59 | return tabs[0].url; 60 | }); 61 | hostname = url_domain(current_url); 62 | 63 | if (!blacklist.includes(hostname)) { 64 | blacklist.push(hostname); 65 | 66 | document.querySelector("#blacklist").innerHTML = 'de-blacklist'; 67 | } 68 | else { 69 | index = blacklist.indexOf(hostname); 70 | if (index > -1){ 71 | blacklist.splice(index,1); 72 | } 73 | document.querySelector("#blacklist").innerHTML = 'blacklist'; 74 | 75 | } 76 | 77 | blacklist = JSON.stringify(blacklist); 78 | e.preventDefault(); 79 | browser.storage.sync.set({ 80 | blacklist: blacklist, 81 | }); 82 | 83 | browser.runtime.reload(); 84 | window.close(); 85 | } 86 | 87 | function onError(error) { 88 | browser.storage.sync.set({ 89 | blacklist: JSON.stringify([""]), 90 | }); 91 | } 92 | 93 | let blacklist = browser.storage.sync.get("blacklist"); 94 | blacklist.then(store, onError); 95 | } 96 | 97 | function restoreOptions() { 98 | function setCurrentChoice(result) { 99 | document.querySelector("#voiceSelect").value = 100 | result.voiceSelect || "en-US"; 101 | } 102 | function setIsOn(result) { 103 | document.querySelector("#isOn").checked = result.isOn || false; 104 | } 105 | 106 | async function setBlacklist(result) { 107 | blacklist = JSON.parse(result.blacklist); 108 | current_url = await browser.tabs 109 | .query({ currentWindow: true, active: true }) 110 | .then((tabs) => { 111 | return tabs[0].url; 112 | }); 113 | hostname = url_domain(current_url); 114 | if (blacklist.includes(hostname)){ 115 | document.querySelector("#blacklist").innerHTML = 'deblacklist'; 116 | } 117 | else{ 118 | document.querySelector("#blacklist").innerHTML = 'blacklist'; 119 | } 120 | } 121 | 122 | function onError(error) {} 123 | let getting = browser.storage.sync.get("voiceSelect"); 124 | getting.then(setCurrentChoice, onError); 125 | let isOn = browser.storage.sync.get("isOn"); 126 | isOn.then(setIsOn, onError); 127 | let blacklist = browser.storage.sync.get("blacklist"); 128 | blacklist.then(setBlacklist, onError); 129 | 130 | } 131 | var isChrome = 132 | /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); 133 | if (!isChrome) { 134 | document.querySelector("#chromeText").remove(); 135 | } 136 | document.querySelector("#voiceSelect").addEventListener("change", saveOptions); 137 | document.querySelector("#isOn").addEventListener("change", saveOptions); 138 | document.querySelector("#blacklist").addEventListener("click", addToBlacklist); 139 | 140 | document.addEventListener("DOMContentLoaded", restoreOptions); 141 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "webextension-polyfill": { 6 | "version": "0.6.0", 7 | "resolved": "https://registry.npmjs.org/webextension-polyfill/-/webextension-polyfill-0.6.0.tgz", 8 | "integrity": "sha512-PlYwiX8e4bNZrEeBFxbFFsLtm0SMPxJliLTGdNCA0Bq2XkWrAn2ejUd+89vZm+8BnfFB1BclJyCz3iKsm2atNg==", 9 | "dev": true 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pronounce.js: -------------------------------------------------------------------------------- 1 | function onError(error) {} 2 | 3 | function onGot(item) { 4 | speechSynthesis.getVoices(); 5 | var voice = "Alex (en-US)"; 6 | if (item.voiceSelect) { 7 | voice = item.voiceSelect.split(" ")[0]; 8 | } 9 | document.ondblclick = async function () { 10 | var sel = 11 | (document.selection && document.selection.createRange().text) || 12 | (window.getSelection && window.getSelection().toString()); 13 | 14 | const regex = /[^\s]+/gm; 15 | const found = sel.match(regex); 16 | let voices = speechSynthesis.getVoices(); 17 | if (found && found.length == 1) { 18 | let utterance = new SpeechSynthesisUtterance(sel); 19 | for (i = 0; i < voices.length; i++) { 20 | if (voices[i].name === voice) { 21 | utterance.voice = voices[i]; 22 | lang = voices[i].lang; 23 | } 24 | } 25 | speechSynthesis.speak(utterance); 26 | addToList(sel,voice,lang); 27 | } 28 | }; 29 | } 30 | function addToList(sel,voice) { 31 | async function store(result) { 32 | if (result.list == null) { 33 | list = []; 34 | } else { 35 | list = JSON.parse(result.list); 36 | } 37 | 38 | if (!list.includes(sel)) { 39 | list.push([sel,voice,lang]); 40 | list = JSON.stringify(list); 41 | browser.storage.sync.set({ 42 | list: list, 43 | }); 44 | } 45 | } 46 | 47 | let list = browser.storage.sync.get("list"); 48 | list.then(store, onError); 49 | } 50 | 51 | function url_domain(data) { 52 | var a = document.createElement("a"); 53 | a.href = data; 54 | return a.hostname; 55 | } 56 | 57 | async function onGotBlacklist(result) { 58 | blacklist = JSON.parse(result.blacklist); 59 | current_url = window.location.href; 60 | hostname = url_domain(current_url); 61 | console.log(current_url); 62 | if (!blacklist.includes(hostname)) { 63 | let isOn = browser.storage.sync.get("isOn"); 64 | isOn.then(onGotIsOn, onError); 65 | } 66 | } 67 | 68 | async function onGotIsOn(isOn) { 69 | if (isOn.isOn == true) { 70 | let getting = browser.storage.sync.get("voiceSelect"); 71 | getting.then(onGot, onError); 72 | } 73 | } 74 | 75 | let blacklist = browser.storage.sync.get("blacklist"); 76 | blacklist.then(onGotBlacklist, onError); 77 | --------------------------------------------------------------------------------