├── .eslintrc.json ├── .github ├── issue_template.md └── stale.yml ├── .gitignore ├── .prettierrc ├── Gruntfile.js ├── LICENSE ├── NOTICE ├── README.md ├── TODO ├── pack_xpi.sh ├── package-lock.json ├── package.json └── src ├── _locales ├── de │ └── messages.json ├── en │ └── messages.json ├── pt_BR │ └── messages.json ├── pt_PT │ └── messages.json ├── ru │ └── messages.json ├── zh_CN │ └── messages.json └── zh_TW │ └── messages.json ├── about.html ├── broken.html ├── css ├── debug.css ├── fontello.css ├── popup.css ├── style.css └── suspended.css ├── debug.html ├── font ├── fontello.woff └── fontello.woff2 ├── history.html ├── img ├── chromeDefaultFavicon.png ├── chromeDefaultFaviconSml.png ├── chromeDevDefaultFavicon.png ├── chromeDevDefaultFaviconSml.png ├── ic_suspendy_128x128.png ├── ic_suspendy_16x16.png ├── ic_suspendy_16x16_grey.png ├── ic_suspendy_32x32.png ├── ic_suspendy_32x32_grey.png ├── ic_suspendy_48x48.png ├── snoozy_tab.svg ├── snoozy_tab_awake.svg ├── suspendy-guy-alt.png ├── suspendy-guy-oops.png ├── suspendy-guy-uh-oh.png └── suspendy-guy.png ├── js ├── about.js ├── background.js ├── broken.js ├── contentscript.js ├── db.js ├── debug.js ├── dom-to-image-more.js ├── gsChrome.js ├── gsFavicon.js ├── gsIndexedDb.js ├── gsMessages.js ├── gsSession.js ├── gsStorage.js ├── gsSuspendedTab.js ├── gsTabCheckManager.js ├── gsTabDiscardManager.js ├── gsTabQueue.js ├── gsTabSuspendManager.js ├── gsUtils.js ├── history.js ├── historyItems.js ├── historyUtils.js ├── html2canvas.js ├── html2canvas.min.js ├── notice.js ├── options.js ├── permissions.js ├── popup.js ├── recovery.js ├── restoring-window.js ├── shortcuts.js ├── tests │ ├── fixture_currentSessions.json │ ├── fixture_previewUrls.json │ ├── fixture_savedSessions.json │ ├── test_createAndUpdateSessionRestorePoint.js │ ├── test_currentSessions.js │ ├── test_gsChrome.js │ ├── test_gsTabQueue.js │ ├── test_gsUtils.js │ ├── test_savedSessions.js │ ├── test_suspendTab.js │ ├── test_trimDbItems.js │ ├── test_updateCurrentSession.js │ └── tests.js ├── update.js └── updated.js ├── manifest.json ├── notice.html ├── options.html ├── permissions.html ├── popup.html ├── recovery.html ├── restoring-window.html ├── shortcuts.html ├── suspended.html ├── tests.html ├── update.html └── updated.html /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "ecmaVersion": 2017 5 | }, 6 | "env": { 7 | "browser": true, 8 | "es6": true, 9 | "webextensions": true 10 | }, 11 | "extends": [ 12 | "eslint:recommended", 13 | "prettier" 14 | ], 15 | "rules": { 16 | "no-console": 0, 17 | "no-unused-vars": [ 18 | "error", 19 | { "vars": "all", "args": "none", "ignoreRestSiblings": false } 20 | ], 21 | "no-undef": ["error"], 22 | "no-proto": ["error"], 23 | // "prefer-arrow-callback": ["warn"], TODO: refactor to use arrow functions 24 | // "no-var": ["error"], TODO: refactor to use let and const 25 | "prefer-spread": ["warn"], 26 | // "semi": ["error", "always"], 27 | "padded-blocks": ["off", { "blocks": "never" }], 28 | // "indent": ["error", 2], 29 | "one-var": ["off", "never"], 30 | "spaced-comment": ["off", "always"] 31 | // "space-before-function-paren": [ 32 | // "error", 33 | // { 34 | // "anonymous": "always", 35 | // "named": "never", 36 | // "asyncArrow": "always" 37 | // } 38 | // ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | Please complete the following information when submitting a feature request or bug report. 2 | * Extension version: 3 | * Browser name & version: 4 | * Operating system & version: 5 | 6 | And please also do a search for your request/bug before create a new one thanks! 7 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 180 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 30 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - blocked 8 | - bug-confirmed 9 | - feature 10 | - pinned 11 | - prioritised 12 | - released 13 | - security 14 | - waiting-on-release 15 | # Label to use when marking an issue as stale 16 | staleLabel: stale 17 | # Comment to post when marking an issue as stale. Set to `false` to disable 18 | markComment: > 19 | This issue has been automatically marked as stale because it has not had any new 20 | activity for 180 days. It will be closed in 30 days if no further activity occurs. 21 | Thank you for your contributions. 22 | # Comment to post when closing a stale issue. Set to `false` to disable 23 | closeComment: false 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/Thumbs.db 2 | **/*.pem 3 | /node_modules 4 | /assets/* 5 | /build/* 6 | /.idea/* 7 | /.debris/* 8 | build/zip/thegreatsuspender-6.30-dev/welcome.html 9 | .DS_Store 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "_COMMENT1": "THE BELOW SHOULD BE THE DEFAULTS BUT IT'S NICE TO SEE WHAT THEY ARE", 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | 6 | 7 | "_COMMENT2": "THE BELOW OVERRIDE PRETTIER'S DEFAULTS", 8 | "singleQuote": true, 9 | "trailingComma": "es5", 10 | } 11 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | require('time-grunt')(grunt); 3 | 4 | grunt.initConfig({ 5 | pkg: grunt.file.readJSON('package.json'), 6 | manifest: grunt.file.readJSON('src/manifest.json'), 7 | config: { 8 | tempDir: 9 | grunt.cli.tasks[0] === 'tgut' ? 'build/tgut-temp/' : 'build/tgs-temp/', 10 | buildName: 11 | grunt.cli.tasks[0] === 'tgut' 12 | ? 'tgut-<%= manifest.version %>' 13 | : 'tgs-<%= manifest.version %>', 14 | }, 15 | copy: { 16 | main: { 17 | expand: true, 18 | src: ['src/**', '!src/tests.html', '!src/js/tests/**'], 19 | dest: '<%= config.tempDir %>', 20 | }, 21 | }, 22 | 'string-replace': { 23 | debugoff: { 24 | files: { 25 | '<%= config.tempDir %>src/js/': 26 | '<%= config.tempDir %>src/js/gsUtils.js', 27 | }, 28 | options: { 29 | replacements: [ 30 | { 31 | pattern: /debugInfo\s*=\s*true/, 32 | replacement: 'debugInfo = false', 33 | }, 34 | { 35 | pattern: /debugError\s*=\s*true/, 36 | replacement: 'debugError = false', 37 | }, 38 | ], 39 | }, 40 | }, 41 | debugon: { 42 | files: { 43 | '<%= config.tempDir %>src/js/': 44 | '<%= config.tempDir %>src/js/gsUtils.js', 45 | }, 46 | options: { 47 | replacements: [ 48 | { 49 | pattern: /debugInfo\s*=\s*false/, 50 | replacement: 'debugInfo = true', 51 | }, 52 | { 53 | pattern: /debugError\s*=\s*false/, 54 | replacement: 'debugError = true', 55 | }, 56 | ], 57 | }, 58 | }, 59 | localesTgut: { 60 | files: { 61 | '<%= config.tempDir %>src/_locales/': 62 | '<%= config.tempDir %>src/_locales/**', 63 | }, 64 | options: { 65 | replacements: [ 66 | { 67 | pattern: /The Great Suspender/gi, 68 | replacement: 'The Great Tester', 69 | }, 70 | ], 71 | }, 72 | }, 73 | }, 74 | crx: { 75 | public: { 76 | src: [ 77 | '<%= config.tempDir %>src/**/*', 78 | '!**/html2canvas.js', 79 | '!**/Thumbs.db', 80 | ], 81 | dest: 'build/zip/<%= config.buildName %>.zip', 82 | }, 83 | private: { 84 | src: [ 85 | '<%= config.tempDir %>src/**/*', 86 | '!**/html2canvas.js', 87 | '!**/Thumbs.db', 88 | ], 89 | dest: 'build/crx/<%= config.buildName %>.crx', 90 | options: { 91 | privateKey: 'key.pem', 92 | }, 93 | }, 94 | }, 95 | clean: ['<%= config.tempDir %>'], 96 | }); 97 | 98 | grunt.loadNpmTasks('grunt-contrib-copy'); 99 | grunt.loadNpmTasks('grunt-string-replace'); 100 | grunt.loadNpmTasks('grunt-crx'); 101 | grunt.loadNpmTasks('grunt-contrib-clean'); 102 | grunt.registerTask('default', [ 103 | 'copy', 104 | 'string-replace:debugoff', 105 | 'crx:public', 106 | 'crx:private', 107 | 'clean', 108 | ]); 109 | grunt.registerTask('tgut', [ 110 | 'copy', 111 | 'string-replace:debugon', 112 | 'string-replace:localesTgut', 113 | 'crx:public', 114 | 'crx:private', 115 | 'clean', 116 | ]); 117 | }; 118 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | src/js/html2canvas.js 2 | src/js/html2canvas.min.js 3 | -- 4 | https://github.com/niklasvh/html2canvas 5 | -- 6 | 7 | Copyright (c) 2012 Niklas von Hertzen 8 | 9 | Permission is hereby granted, free of charge, to any person 10 | obtaining a copy of this software and associated documentation 11 | files (the "Software"), to deal in the Software without 12 | restriction, including without limitation the rights to use, 13 | copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the 15 | Software is furnished to do so, subject to the following 16 | conditions: 17 | 18 | The above copyright notice and this permission notice shall be 19 | included in all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 23 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 25 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 26 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 27 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 28 | OTHER DEALINGS IN THE SOFTWARE. 29 | 30 | ------------------------------------------------------------- 31 | 32 | src/js/db.js 33 | -- 34 | https://github.com/aaronpowell/db.js 35 | -- 36 | 37 | The MIT License 38 | 39 | Copyright (c) 2012 Aaron Powell 40 | 41 | Permission is hereby granted, free of charge, to any person 42 | obtaining a copy of this software and associated documentation 43 | files (the "Software"), to deal in the Software without 44 | restriction, including without limitation the rights to use, 45 | copy, modify, merge, publish, distribute, sublicense, and/or sell 46 | copies of the Software, and to permit persons to whom the 47 | Software is furnished to do so, subject to the following 48 | conditions: 49 | 50 | The above copyright notice and this permission notice shall be 51 | included in all copies or substantial portions of the Software. 52 | 53 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 54 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 55 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 56 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 57 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 58 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 59 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 60 | OTHER DEALINGS IN THE SOFTWARE. 61 | 62 | ------------------------------------------------------------- 63 | 64 | src/js/dom-to-image.js 65 | src/js/dom-to-image-more.js 66 | -- 67 | https://github.com/tsayen/dom-to-image 68 | https://github.com/1904labs/dom-to-image-more 69 | -- 70 | 71 | The MIT License (MIT) 72 | 73 | Copyright 2018 Marc Brooks 74 | https://about.me/idisposable 75 | 76 | Copyright 2015 Anatolii Saienko 77 | https://github.com/tsayen 78 | 79 | Copyright 2012 Paul Bakaus 80 | http://paulbakaus.com/ 81 | 82 | Permission is hereby granted, free of charge, to any person obtaining 83 | a copy of this software and associated documentation files (the 84 | "Software"), to deal in the Software without restriction, including 85 | without limitation the rights to use, copy, modify, merge, publish, 86 | distribute, sublicense, and/or sell copies of the Software, and to 87 | permit persons to whom the Software is furnished to do so, subject to 88 | the following conditions: 89 | 90 | The above copyright notice and this permission notice shall be 91 | included in all copies or substantial portions of the Software. 92 | 93 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 94 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 95 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 96 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 97 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 98 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 99 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unofficial The Great Suspender for Firefox 2 | 3 | This is a work-in-progress port of "The Great Suspender" WebExtension for Mozilla Firefox and compatible browsers. 4 | Current TODO could be found in [TODO](TODO). 5 | You may also submit a bug or pull request via Github. 6 | 7 | ## Debugging 8 | 9 | You may load it as a temporary extension at `about:debugging` 10 | 11 | ## Building from source 12 | 13 | There are two options 14 | 1. Use npm as described in the original README and rename zip/crx into xpi 15 | 2. Use `pack_xpi.sh` or do the same manually 16 | 17 | **NOTE**: to install an unsigned xpi you should use Developer or custom build of Firefox with disabled extension signing enforcement. It may also work on some GNU/Linux distros. *Search for it yourself if required*. 18 | 19 | 20 | The rest of README file carried from the original project without change. However **do not** submit bug reports about *this* project to *original author's repository*. 21 | 22 | # The Great Suspender 23 | 24 | 25 | 26 | "The Great Suspender" is a free and open-source Google Chrome extension for people who find that chrome is consuming too much system resource or suffer from frequent chrome crashing. Once installed and enabled, this extension will automatically *suspend* tabs that have not been used for a while, freeing up memory and cpu that the tab was consuming. 27 | 28 | If you have suggestions or problems using the extension, please [submit a bug or a feature request](https://github.com/deanoemcke/thegreatsuspender/issues/). For other enquiries you can email me at greatsuspender@gmail.com. 29 | 30 | ### New release! 31 | I am currently rolling out a new chrome webstore release. 32 | 33 | **If you have lost tabs from your browser:** I have written a guide for how to recover your lost tabs [here](https://github.com/deanoemcke/thegreatsuspender/issues/526 34 | ). 35 | 36 | ### Chrome Web Store 37 | 38 | The Great Suspender is [available via the official Chrome Web Store](https://chrome.google.com/webstore/detail/the-great-suspender/klbibkeccnjlkjkiokjodocebajanakg). 39 | 40 | Please note that the webstore version may be behind the latest version here. That is because I try to keep webstore updates down to a minimum due to their [disruptive effect](https://github.com/deanoemcke/thegreatsuspender/issues/526). 41 | 42 | For more information on the permissions required for the extension, please refer to this gitHub issue: (https://github.com/deanoemcke/thegreatsuspender/issues/213) 43 | 44 | ### Install as an extension from source 45 | 46 | 1. Download the **[latest available version](https://github.com/deanoemcke/thegreatsuspender/releases)** and unarchive to your preferred location (whichever suits you). 47 | 2. Using **Google Chrome** browser, navigate to chrome://extensions/ and enable "Developer mode" in the upper right corner. 48 | 3. Click on the Load unpacked extension... button. 49 | 4. Browse to the src directory of the unarchived folder and confirm. 50 | 51 | If you have completed the above steps, the "welcome" page will open indicating successful installation of the extension. 52 | 53 | Be sure to unsuspend all suspended tabs before removing any other version of the extension or they will disappear forever! 54 | 55 | ### Build from github 56 | 57 | Dependencies: openssl, npm. 58 | 59 | Clone the repository and run these commands: 60 | ``` 61 | npm install 62 | npm run generate-key 63 | npm run build 64 | ``` 65 | 66 | It should say: 67 | ``` 68 | Done, without errors. 69 | ``` 70 | 71 | The extension in crx format will be inside the build/crx/ directory. You can drag it into [extensions] (chrome://extensions) to install locally. 72 | 73 | ### Integrating with another Chrome extension or app 74 | 75 | This extension has a small external api to allow other extensions to request the suspension of a tab. See [this issue](https://github.com/deanoemcke/thegreatsuspender/issues/276) for more information. And please let me know about it so that I can try it out! 76 | 77 | ### Contributing to this extension 78 | 79 | Contributions are very welcome. Feel free to submit pull requests for new features and bug fixes. For new features, ideally you would raise an issue for the proposed change first so that we can discuss ideas. This will go a long way to ensuring your pull request is accepted. 80 | 81 | ### License 82 | 83 | This work is licensed under a GNU GENERAL PUBLIC LICENSE (v2) 84 | 85 | ### Shoutouts 86 | 87 | This package uses the [html2canvas](https://github.com/niklasvh/html2canvas) library written by Niklas von Hertzen. 88 | It also uses the indexedDb wrapper [db.js](https://github.com/aaronpowell/db.js) written by Aaron Powell. 89 | Thank you also to [BrowserStack](https://www.browserstack.com) for providing free chrome testing tools. 90 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | + Done 2 | - Discarded 3 | * New 4 | H On hold 5 | 6 | + Remove autodicardable to avoid code breaking in Firefox 7 | H Keep up with Firefox changes on autodiscardable: unsupported as of 2019.08 8 | + Fix screenshots: check Firefox Screenshots 9 | + Fix Firefox shortcut: Ctrl+Shift+S confilicts with Firefox Screenshots 10 | * Rebrand: name 11 | * Rebrand: logo 12 | * Migrate to building with Makefile 13 | * Publish to AMO 14 | -------------------------------------------------------------------------------- /pack_xpi.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mkdir -p build/xpi 3 | cd src 4 | find -not -name 'html2canvas.min*' -not -name 'Thumbs.db' | xargs zip -q ../build/xpi/tgs-firefox.xpi 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thegreatsuspender", 3 | "version": "0.0.0", 4 | "description": "A chrome extension for suspending all tabs to free up memory", 5 | "main": "", 6 | "scripts": { 7 | "build": "grunt", 8 | "generate-key": "openssl genrsa -out key.pem", 9 | "test": "echo \"Error: no test specified\" && exit 1", 10 | "eslint-check": "eslint --print-config .eslintrc.js | eslint-config-prettier-check" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git://github.com/deanoemcke/thegreatsuspender.git" 15 | }, 16 | "keywords": [ 17 | "chrome", 18 | "extension", 19 | "addon", 20 | "memory", 21 | "suspend", 22 | "tab" 23 | ], 24 | "author": "deanoemcke", 25 | "license": "GPLv2", 26 | "bugs": { 27 | "url": "https://github.com/deanoemcke/thegreatsuspender/issues" 28 | }, 29 | "devDependencies": { 30 | "eslint": "^4.19.1", 31 | "eslint-config-prettier": "^2.9.0", 32 | "eslint-config-standard": "^10.2.1", 33 | "eslint-plugin-import": "^2.7.0", 34 | "eslint-plugin-node": "^5.1.1", 35 | "eslint-plugin-promise": "^3.5.0", 36 | "eslint-plugin-standard": "^3.0.1", 37 | "grunt": "~0.4.5", 38 | "grunt-cli": "^1.2.0", 39 | "grunt-contrib-clean": "^1.1.0", 40 | "grunt-contrib-copy": "^1.0.0", 41 | "grunt-crx": "~1.0.5", 42 | "grunt-string-replace": "^1.3.1", 43 | "prettier": "1.13.7", 44 | "time-grunt": "~1.2.1" 45 | }, 46 | "dependencies": { 47 | "db.js": "^0.15.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 19 | 20 |
21 | 35 |
36 | 37 |
38 | 39 |

40 | 41 |

42 | 43 | github.com/dvalter/thegreatsuspender 44 |
45 |

46 | 47 |

48 | 49 | 50 | 51 |

52 | 53 |

54 | 55 | 56 |

57 | 58 |
59 |
60 |

61 | 62 | 63 | 64 |
65 | 66 | 67 | 68 |
69 | 70 | 71 | 72 |
73 | 78 |

79 |
80 | 81 |
82 | 83 |
84 | 85 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/broken.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | The Great Suspender is broken 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 |

Ruh Roh!

19 |

The Great Suspender failed to start. Perhaps you are using an incompatible version of chrome?

20 |

Try to restart the extension. If the problem persists, ask for help on the GitHub project page.

21 |

You can recover lost tabs from the session management page.

22 |
23 | 24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/css/debug.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 20px; 3 | font-size: 1.3rem; 4 | } 5 | 6 | td { 7 | max-width:700px; 8 | white-space:nowrap; 9 | overflow:hidden; 10 | text-overflow:ellipsis; 11 | } 12 | 13 | img { 14 | height: 16px; 15 | width: 16px; 16 | } -------------------------------------------------------------------------------- /src/css/fontello.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('../font/fontello.woff2?80430491') format('woff2'), 4 | url('../font/fontello.woff?80430491') format('woff'); 5 | font-weight: normal; 6 | font-style: normal; 7 | } 8 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 9 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 10 | /* 11 | @media screen and (-webkit-min-device-pixel-ratio:0) { 12 | @font-face { 13 | font-family: 'fontello'; 14 | src: url('../font/fontello.svg?80430491#fontello') format('svg'); 15 | } 16 | } 17 | */ 18 | 19 | [class^='icon-']:before, 20 | [class*=' icon-']:before { 21 | font-family: 'fontello'; 22 | font-style: normal; 23 | font-weight: normal; 24 | speak: none; 25 | 26 | display: inline-block; 27 | text-decoration: inherit; 28 | width: 1em; 29 | margin-right: 0.2em; 30 | text-align: center; 31 | /* opacity: .8; */ 32 | 33 | /* For safety - reset parent styles, that can break glyph codes*/ 34 | font-variant: normal; 35 | text-transform: none; 36 | 37 | /* fix buttons height, for twitter bootstrap */ 38 | line-height: 1em; 39 | 40 | /* Animation center compensation - margins should be symmetric */ 41 | /* remove if not needed */ 42 | margin-left: 0.2em; 43 | 44 | /* you can be more comfortable with increased icons size */ 45 | /* font-size: 120%; */ 46 | 47 | /* Font smoothing. That was taken from TWBS */ 48 | -webkit-font-smoothing: antialiased; 49 | -moz-osx-font-smoothing: grayscale; 50 | 51 | /* Uncomment for 3D effect */ 52 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 53 | } 54 | 55 | .icon-help-circled:before { 56 | content: '\e80a'; 57 | } /* '' */ 58 | .icon-check-empty:before { 59 | content: '\f096'; 60 | } /* '' */ 61 | .icon-sort-down:before { 62 | content: '\f0dd'; 63 | } /* '' */ 64 | .icon-minus-squared-alt:before { 65 | content: '\f147'; 66 | } /* '' */ 67 | .icon-ok-squared:before { 68 | content: '\f14a'; 69 | } /* '' */ 70 | .icon-plus-squared-alt:before { 71 | content: '\f196'; 72 | } /* '' */ 73 | -------------------------------------------------------------------------------- /src/css/popup.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | - - /|_/| .-------------------. 4 | - _______________| @.@| / Styles by ) 5 | -- (______ >\_C/< ---/ Liam Johnston / 6 | - - / ______ _/____) ( @liamjohnstonnz / 7 | - - / /\ \ \ \ `-------------------' 8 | - (_/ \_) - \_) 9 | 10 | */ 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | } 16 | html { 17 | box-sizing: border-box; 18 | } 19 | *, 20 | *::before, 21 | *::after { 22 | box-sizing: inherit; 23 | } 24 | body { 25 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 26 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 27 | 'Segoe UI Symbol'; 28 | color: #444; 29 | padding: 0; 30 | margin: 0; 31 | min-width: 325px; 32 | position: relative; 33 | font-size: 15px; 34 | background: #fff; 35 | } 36 | @media (min-resolution: 192dpi) { 37 | body { 38 | font-weight: 300; 39 | } 40 | } 41 | 42 | a { 43 | outline: none; 44 | color: #3477db; 45 | } 46 | #popupContent { 47 | opacity: 0; 48 | transition: opacity 200ms ease; 49 | } 50 | #statusDetail a { 51 | color: #ffffff; 52 | } 53 | #header { 54 | padding: 15px; 55 | background: #777; 56 | color: #fff; 57 | } 58 | #header.willSuspend { 59 | background: #3477db; 60 | } 61 | #header.blockedFile { 62 | background: #a70707; 63 | } 64 | 65 | .group { 66 | padding: 10px 0; 67 | } 68 | .group:not(:last-of-type) { 69 | border-bottom: 1px solid #ccc; 70 | } 71 | 72 | .menuOption { 73 | line-height: 2em; 74 | font-size: 16px; 75 | white-space: nowrap; 76 | cursor: pointer; 77 | padding: 0 15px; 78 | } 79 | .menuOption:hover .optionText { 80 | text-decoration: underline; 81 | } 82 | /* dark theme for night lurkers */ 83 | .dark { 84 | background: #353535; 85 | color: #b8b8b8; 86 | } 87 | .dark .group { 88 | border-color: #555; 89 | } 90 | -------------------------------------------------------------------------------- /src/css/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | - - /|_/| .-------------------. 4 | - _______________| @.@| / Styles by ) 5 | -- (______ >\_C/< ---/ Liam Johnston / 6 | - - / ______ _/____) ( @liamjohnstonnz / 7 | - - / /\ \ \ \ `-------------------' 8 | - (_/ \_) - \_) 9 | 10 | */ 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | min-height: 100%; 16 | font-size: 10px; 17 | } 18 | html { 19 | box-sizing: border-box; 20 | height: 100vh; 21 | } 22 | *, 23 | *::before, 24 | *::after { 25 | box-sizing: inherit; 26 | } 27 | body { 28 | min-height: 100%; 29 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 30 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 31 | 'Segoe UI Symbol'; 32 | font-size: 1.5rem; 33 | line-height: 1.5em; 34 | background: #fafafa; 35 | color: #444; 36 | } 37 | @media (min-resolution: 192dpi) { 38 | body { 39 | font-weight: 300; 40 | } 41 | } 42 | /* 'content' pages alignment (suspended tabs have their own overrides) */ 43 | /* apply to body tag */ 44 | .splash-wrap { 45 | display: flex; 46 | justify-content: center; 47 | /* 48 | vertical center usually looks a little better but some pages have content 49 | which changes height (accordion etc) so this doens't really work 50 | */ 51 | /* align-items: center; */ 52 | } 53 | strong { 54 | font-weight: 600; 55 | } 56 | small { 57 | font-size: smaller; 58 | } 59 | h1 { 60 | font-size: 26px; 61 | margin-bottom: 20px; 62 | } 63 | p { 64 | margin: 0 0 15px; 65 | } 66 | a { 67 | color: #3477db; 68 | text-decoration: none; 69 | cursor: pointer; 70 | } 71 | a:hover { 72 | text-decoration: underline; 73 | } 74 | a.active { 75 | cursor: default; 76 | color: #444; 77 | pointer-events: none; 78 | } 79 | hr { 80 | border-width: 0; 81 | border-top: 1px solid #e7e7e7; 82 | height: 1px; 83 | margin: 23px 0; 84 | } 85 | .fl { 86 | float: left; 87 | margin-right: 10px; 88 | } 89 | .fr { 90 | float: right; 91 | margin-left: 10px; 92 | } 93 | .topLabel { 94 | display: inline-block; 95 | margin: 0 0 8px; 96 | } 97 | select { 98 | min-width: 150px; 99 | color: #444; 100 | background: #fff; 101 | border-radius: 3px; 102 | -webkit-appearance: none; 103 | appearance: none; 104 | margin: 0; 105 | padding: 10px 30px 10px 15px; 106 | font-size: 14px; 107 | border-color: #ccc; 108 | position: relative; 109 | cursor: pointer; 110 | } 111 | .select-wrapper { 112 | background-color: #fff; 113 | display: inline-block; 114 | position: relative; 115 | } 116 | .select-wrapper::after { 117 | position: absolute; 118 | right: 15px; 119 | top: 6px; 120 | font-family: 'fontello'; 121 | text-decoration: none; 122 | content: '\f0dd'; 123 | color: #3477db; 124 | pointer-events: none; 125 | } 126 | textarea { 127 | color: #444; 128 | width: 100%; 129 | border-color: #c3c3c3; 130 | font-size: 14px; 131 | padding: 3px 5px; 132 | white-space: nowrap; 133 | margin-top: 9px; 134 | } 135 | .formRow { 136 | margin: 0 0 20px; 137 | } 138 | ul.unorderedList, 139 | ol.orderedList { 140 | padding-left: 20px; 141 | margin: 7px 0 10px 0; 142 | } 143 | ul.unorderedList li { 144 | margin: 0 0 3 10px; 145 | list-style: disc; 146 | } 147 | .splash { 148 | padding: 0 20px; 149 | margin: 50px auto; 150 | display: grid; 151 | grid-template-columns: 150px 1fr; 152 | grid-gap: 30px; 153 | } 154 | .splash:not(.welcome-message) { 155 | width: 900px; 156 | } 157 | .suspendy-guy { 158 | width: 100%; 159 | } 160 | #suspendy-guy-inprogress { 161 | max-height: 220px; 162 | max-width: 137px; 163 | } 164 | #suspendy-guy-complete { 165 | max-height: 220px; 166 | } 167 | .btn { 168 | background: #3477db; 169 | color: #fff; 170 | border-radius: 3px; 171 | height: 40px; 172 | line-height: 40px; 173 | padding: 0 20px; 174 | display: inline-block; 175 | border: 0; 176 | font-size: 14px; 177 | font-weight: 500; 178 | cursor: pointer; 179 | 180 | min-width: 80px; 181 | text-align: center; 182 | } 183 | .btn:hover { 184 | background: #5c9dfe; 185 | text-decoration: none; 186 | } 187 | .btn.btnNeg { 188 | background: #ddd; 189 | color: #333; 190 | } 191 | .btn.btnNeg:hover { 192 | background: #ccc; 193 | } 194 | 195 | .btnDisabled { 196 | background: #f1f1f1; 197 | color: #888; 198 | cursor: default; 199 | } 200 | .btnDisabled:hover { 201 | background: #f1f1f1; 202 | color: #888; 203 | } 204 | .lesserText { 205 | color: #999; 206 | } 207 | /* oh dear. well, too late now... */ 208 | .hidden { 209 | visibility: hidden; 210 | } 211 | .reallyHidden { 212 | display: none; 213 | } 214 | .mainContent { 215 | width: 750px; 216 | margin: 50px auto; 217 | display: grid; 218 | /* defining first col width because 'auto' could change due to bold text on active item */ 219 | grid-template-columns: 200px auto; 220 | grid-column-gap: 40px; 221 | grid-template-areas: 222 | 'h h' 223 | 'n m'; 224 | } 225 | .pageHeader { 226 | grid-area: h; 227 | margin: 0 0 50px; 228 | font-size: 20px; 229 | } 230 | .contentNav { 231 | grid-area: n; 232 | border-right: 1px solid #ddd; 233 | padding-right: 15px; 234 | align-self: start; 235 | } 236 | .contentNav ul { 237 | padding: 0; 238 | margin: 0; 239 | list-style-type: none; 240 | } 241 | .contentNav a { 242 | font-size: 16px; 243 | margin-bottom: 20px; 244 | color: #888; 245 | display: block; 246 | } 247 | .contentNav .active { 248 | color: #3477db; 249 | font-weight: bold; 250 | } 251 | .content { 252 | grid-area: m; 253 | font-size: 14px; 254 | } 255 | .content h2 { 256 | font-size: 16px; 257 | font-weight: bold; 258 | margin: 0 0 30px; 259 | } 260 | .heading-note { 261 | font-style: italic; 262 | margin-top: -25px; 263 | margin-bottom: 25px; 264 | } 265 | .welcome-message { 266 | border-radius: 6px; 267 | background: #fff; 268 | padding: 30px; 269 | margin-bottom: 20px; 270 | align-items: center; 271 | grid-gap: 40px; 272 | box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); 273 | } 274 | .gsNote { 275 | font-style: italic; 276 | font-size: 12px; 277 | background: rgb(255, 247, 202); 278 | padding: 5px; 279 | margin-top: 5px; 280 | } 281 | .sessionLink { 282 | cursor: pointer; 283 | } 284 | .sessionLink:hover { 285 | text-decoration: underline; 286 | } 287 | .tabContainer { 288 | padding: 0px; 289 | margin: 0 0 5px 0; 290 | } 291 | .tabContainer > * { 292 | vertical-align: middle; 293 | } 294 | a.historyLink { 295 | padding-left: 5px; 296 | color: #333; 297 | font-size: 13px; 298 | text-decoration: none; 299 | max-width: 90%; 300 | overflow: hidden; 301 | display: inline-block; 302 | white-space: nowrap; 303 | text-overflow: ellipsis; 304 | } 305 | a.historyLink:hover { 306 | text-decoration: underline; 307 | } 308 | .windowContainer { 309 | font-weight: bold; 310 | margin: 20px 0 10px; 311 | } 312 | 313 | .groupLink { 314 | margin: 0 0 0 10px; 315 | font-size: 11px; 316 | font-weight: normal; 317 | } 318 | .sessionContainer { 319 | white-space: nowrap; 320 | margin: 0 0 5px; 321 | } 322 | .sessionContainer .groupLink { 323 | visibility: hidden; 324 | } 325 | .sessionContainer:hover .groupLink { 326 | visibility: visible; 327 | } 328 | .sessionContents { 329 | margin-left: 28px; 330 | } 331 | /* conatiner for a group of sessions */ 332 | .sessionsContainer { 333 | margin-bottom: 50px; 334 | } 335 | .sessionIcon { 336 | cursor: pointer; 337 | margin: 0 10px 0 -3px; 338 | } 339 | .sessionContents div:last-child { 340 | padding-bottom: 10px; 341 | } 342 | .tabContainer .itemHover { 343 | visibility: hidden; 344 | margin: 0; 345 | padding: 0 0 3px; 346 | cursor: pointer; 347 | } 348 | .tabContainer:hover .itemHover { 349 | visibility: visible; 350 | } 351 | .tabContainer .removeLink { 352 | margin-left: -20px; 353 | color: #888; 354 | } 355 | #screenCaptureNotice { 356 | display: none; 357 | clear: left; 358 | margin-top: 30px; 359 | background: rgb(255, 247, 202); 360 | padding: 10px 0 10px 10px; 361 | } 362 | .keyboardShortcuts { 363 | display: grid; 364 | grid-template-columns: auto auto; 365 | } 366 | .keyboardShortcuts div { 367 | margin: 0 0 2px; 368 | } 369 | .bottomMargin { 370 | margin-bottom: 20px !important; 371 | } 372 | .hotkeyCommand { 373 | word-spacing: -1px; 374 | } 375 | 376 | /* custom checkboxes */ 377 | input[type='checkbox'] { 378 | position: absolute; 379 | opacity: 0; 380 | } 381 | input[type='checkbox'] + label { 382 | position: relative; 383 | padding-left: 30px; 384 | cursor: pointer; 385 | } 386 | input[type='checkbox'] + label:before { 387 | position: absolute; 388 | left: 0; 389 | top: -2px; 390 | font-family: 'fontello'; 391 | text-decoration: none; 392 | content: '\f096'; 393 | color: #888; 394 | font-size: 24px; 395 | cursor: pointer; 396 | } 397 | input[type='checkbox']:checked + label:before { 398 | content: '\f14a'; 399 | color: #3477db; 400 | } 401 | 402 | /* custom radio btns */ 403 | .radio { 404 | position: relative; 405 | display: block; 406 | margin-top: 15px; 407 | margin-bottom: 15px; 408 | } 409 | .radio input[type='radio'] { 410 | opacity: 0; 411 | z-index: 1; 412 | } 413 | .radio input[type='radio']:checked + label::before { 414 | border-color: #3477db; 415 | } 416 | .radio input[type='radio']:checked + label::after { 417 | transform: scale(1, 1); 418 | } 419 | .radio input[type='radio']:checked + label::after { 420 | background-color: #3477db; 421 | } 422 | .radio label { 423 | display: inline-block; 424 | vertical-align: middle; 425 | position: relative; 426 | padding-left: 5px; 427 | cursor: pointer; 428 | } 429 | .radio label::before { 430 | cursor: pointer; 431 | content: ''; 432 | display: inline-block; 433 | position: absolute; 434 | width: 17px; 435 | height: 17px; 436 | left: 0; 437 | top: 4px; 438 | margin-left: -20px; 439 | border: 1px solid #ccc; 440 | border-radius: 50%; 441 | background-color: #fff; 442 | } 443 | .radio label::after { 444 | display: inline-block; 445 | position: absolute; 446 | content: ' '; 447 | width: 13px; 448 | height: 13px; 449 | left: 2px; 450 | top: 6px; 451 | margin-left: -20px; 452 | border-radius: 50%; 453 | background-color: #555; 454 | transform: scale(0, 0); 455 | } 456 | .sub-section { 457 | margin-bottom: 35px; 458 | } 459 | 460 | .tooltipIcon { 461 | color: #444; 462 | } 463 | /* Tooltips */ 464 | [data-i18n-tooltip] { 465 | position: relative; 466 | } 467 | [data-i18n-tooltip]::after { 468 | content: attr(data-i18n-tooltip); 469 | position: absolute; 470 | left: 50%; 471 | top: -6px; 472 | transform: translateX(-50%) translateY(-100%); 473 | background: #fff; 474 | line-height: 20px; 475 | color: #444; 476 | padding: 4px 2px; 477 | min-width: 500px; 478 | border-radius: 4px; 479 | border: 1px solid #e8e8e8; 480 | pointer-events: none; 481 | padding: 24px 24px 32px; 482 | z-index: 99; 483 | opacity: 0; 484 | white-space: pre; 485 | } 486 | [data-i18n-tooltip]:hover::after { 487 | opacity: 1; 488 | } 489 | @keyframes spinner { 490 | to {transform: rotate(360deg);} 491 | } 492 | .faviconSpinner { 493 | position: relative; 494 | display: inline-block; 495 | min-width: 16px; 496 | min-height: 16px; 497 | margin-top: 4px; 498 | } 499 | .faviconSpinner:before { 500 | content: ''; 501 | box-sizing: border-box; 502 | position: absolute; 503 | top: 50%; 504 | left: 50%; 505 | width: 16px; 506 | height: 16px; 507 | margin-top: -10px; 508 | margin-left: -10px; 509 | border-radius: 50%; 510 | border: 2px solid #ccc; 511 | border-top-color: #333; 512 | animation: spinner .6s linear infinite; 513 | } -------------------------------------------------------------------------------- /src/css/suspended.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | - - /|_/| .-------------------. 4 | - _______________| @.@| / Styles by ) 5 | -- (______ >\_C/< ---/ Liam Johnston / 6 | - - / ______ _/____) ( @liamjohnstonnz / 7 | - - / /\ \ \ \ `-------------------' 8 | - (_/ \_) - \_) 9 | 10 | */ 11 | html, 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | min-height: 100%; 16 | font-size: 10px; 17 | } 18 | html { 19 | box-sizing: border-box; 20 | height: 100vh; 21 | } 22 | *, 23 | *::before, 24 | *::after { 25 | box-sizing: inherit; 26 | } 27 | body { 28 | min-height: 100%; 29 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', 30 | Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 31 | 'Segoe UI Symbol'; 32 | font-size: 1.5rem; 33 | line-height: 1.5em; 34 | background: #fafafa; 35 | color: #444; 36 | } 37 | @media (min-resolution: 192dpi) { 38 | body { 39 | font-weight: 300; 40 | } 41 | } 42 | h1 { 43 | font-size: 26px; 44 | margin-bottom: 20px; 45 | } 46 | p { 47 | margin: 0 0 15px; 48 | } 49 | a { 50 | color: #3477db; 51 | text-decoration: none; 52 | cursor: pointer; 53 | } 54 | a:hover { 55 | text-decoration: underline; 56 | } 57 | a.active { 58 | cursor: default; 59 | color: #444; 60 | pointer-events: none; 61 | } 62 | body.hide-initially { 63 | display: none !important; 64 | } 65 | body.suspended-page:not(.img-preview-mode) { 66 | display: flex; 67 | align-items: center; 68 | justify-content: center; 69 | flex-direction: column; 70 | position: absolute; 71 | height: 100%; 72 | width: 100%; 73 | overflow-x: hidden; 74 | overflow-y: hidden; 75 | } 76 | .watermark { 77 | display: none; /* show only if not image preview mode */ 78 | position: absolute; 79 | bottom: 30px; 80 | right: 30px; 81 | font-weight: bold; 82 | color: #b6b6b6; 83 | cursor: pointer; 84 | } 85 | .suspended-page:not(.img-preview-mode) .watermark { 86 | display: block; 87 | } 88 | .gsTopBar { 89 | position: fixed; 90 | background: #fafafa; 91 | top: 0; 92 | padding: 30px 40px 20px; 93 | width: 100%; 94 | text-align: center; 95 | z-index: 101; 96 | } 97 | .faviconWrap { 98 | display: inline-block; 99 | vertical-align: bottom; 100 | margin-bottom: -1px; 101 | margin-right: 10px; 102 | } 103 | .dark .faviconWrapLowContrast { 104 | filter: invert(1) grayscale(1); 105 | } 106 | .gsTopBarImg { 107 | height: 16px; 108 | width: 16px; 109 | } 110 | .gsTopBarTitle { 111 | color: #444; 112 | font-size: 20px; 113 | cursor: default; 114 | } 115 | .gsTopBarUrl { 116 | color: #444; 117 | cursor: pointer; 118 | padding: 0 20px; 119 | } 120 | .gsTopBarUrl, 121 | .gsTopBarTitleWrap { 122 | max-width: 100%; 123 | overflow: hidden; 124 | white-space: nowrap; 125 | text-overflow: ellipsis; 126 | display: inline-block; 127 | margin-bottom: 8px; 128 | } 129 | .hideOverflow { 130 | overflow: hidden; 131 | } 132 | .gsPreviewContainer { 133 | width: 90%; 134 | margin: 0 auto; 135 | padding: 30px 0; 136 | } 137 | .gsPreviewImg { 138 | display: block; 139 | max-width: 100%; 140 | margin: 0 auto; 141 | border-radius: 20px; 142 | overflow: hidden; 143 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); 144 | transition: all 0.2s ease; 145 | } 146 | .gsPreviewImg:hover { 147 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); 148 | } 149 | .dark .gsPreviewImg { 150 | filter: brightness(70%); 151 | opacity: 0.7; 152 | } 153 | .dark .gsPreviewImg:hover { 154 | opacity: 1; 155 | } 156 | body.img-preview-mode .gsTopBar { 157 | position: relative; 158 | } 159 | .suspended-page { 160 | cursor: pointer; 161 | } 162 | .suspendedMsg { 163 | width: 100vw; 164 | height: 100vh; 165 | line-height: 30px; 166 | display: flex; 167 | align-items: center; 168 | justify-content: center; 169 | flex-direction: column; 170 | text-align: center; 171 | } 172 | .hotkeyCommand { 173 | word-spacing: -1px; 174 | } 175 | .reasonMsg { 176 | font-size: 15px; 177 | margin-bottom: 25px; 178 | } 179 | .suspendedMsg img { 180 | height: 180px; 181 | margin-bottom: 30px; 182 | } 183 | .suspendedMsg-instr { 184 | font-size: 20px; 185 | } 186 | .suspendedMsg-shortcut { 187 | font-size: 15px; 188 | } 189 | 190 | .spinner:before { 191 | content: ''; 192 | box-sizing: border-box; 193 | position: fixed; 194 | top: 50%; 195 | left: 50%; 196 | width: 80px; 197 | height: 80px; 198 | margin-top: -40px; 199 | margin-left: -40px; 200 | border-radius: 50%; 201 | border: 8px solid transparent; 202 | border-top-color: #3477db; 203 | animation: spinner 0.6s linear infinite; 204 | z-index: 100; 205 | } 206 | .snoozyWrapper { 207 | position: relative; 208 | } 209 | #snoozySpinner.spinner:before { 210 | border: 2px solid transparent; 211 | border-right-color: #4a4a4a; 212 | border-top-color: #4a4a4a; 213 | animation: spinner 0.6s linear infinite; 214 | margin: 0; 215 | position: absolute; 216 | top: 49px; 217 | right: 7px; 218 | left: auto; 219 | width: 12px; 220 | height: 12px; 221 | } 222 | .suspendedTextWrap { 223 | height: 60px; /* stops slight jump when suspending */ 224 | } 225 | .waking .suspendedTextWrap { 226 | opacity: 0; 227 | } 228 | @keyframes spinner { 229 | to { 230 | transform: rotate(360deg); 231 | } 232 | } 233 | 234 | /* dark theme for night lurkers */ 235 | .dark, 236 | .dark .gsTopBar, 237 | .dark .suspendedMsg { 238 | background: #222; 239 | } 240 | .dark .suspendedMsg img { 241 | filter: brightness(150%); 242 | } 243 | .dark .gsTopBar, 244 | .dark .gsTopBarTitle, 245 | .dark .gsTopBar a, 246 | .dark .suspendedMsg, 247 | .dark .watermark { 248 | color: #b8b8b8; 249 | } 250 | .dark #setKeyboardShortcut { 251 | text-decoration: underline; 252 | color: #b8b8b8; 253 | } 254 | 255 | /* end suspended tab styles */ 256 | 257 | .toast-wrapper { 258 | display: none; 259 | text-align: center; 260 | position: fixed; 261 | z-index: 9999999; 262 | left: 0; 263 | top: 0; 264 | width: 100%; 265 | height: 100%; 266 | opacity: 0; 267 | animation: fadeinout 4s linear forwards; 268 | } 269 | .toast-content { 270 | display: inline-block; 271 | position: relative; 272 | top: 50%; 273 | transform: translateY(-50%); 274 | background-color: #fefefe; 275 | margin: auto; 276 | padding: 10px 20px 20px 20px; 277 | border: 1px solid #888; 278 | border-radius: 5px; 279 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19); 280 | } 281 | 282 | .toast-content p { 283 | font-size: 16px; 284 | } 285 | 286 | @keyframes fadeinout { 287 | 0%, 288 | 100% { 289 | opacity: 0; 290 | visibility: hidden; 291 | } 292 | 5%, 293 | 90% { 294 | opacity: 1; 295 | visibility: visible; 296 | } 297 | } 298 | 299 | @keyframes fadein { 300 | from { 301 | opacity: 0; 302 | } 303 | to { 304 | opacity: 1; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /src/debug.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | The Great Suspender - Debugger 9 | 10 | 11 | 12 | 13 |
14 | debugErrors: 15 |  |  16 | debugInfo: 17 |  |  18 | discardInPlaceOfSuspend: 19 |  |  20 | useAlternateScreenCaptureLib: 21 |  |  22 | claim all suspended tabs 23 |
24 |
25 |
26 | To view debug messages, go to the 27 |
28 | Ensure "developer mode" is checked at the top of the page, then click on the "Inspect views: background page" link for this extension. 29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 |
WinIdTabIdIndexTitleTime to suspendStatus
46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/font/fontello.woff -------------------------------------------------------------------------------- /src/font/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/font/fontello.woff2 -------------------------------------------------------------------------------- /src/history.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 21 | 22 |
23 | 37 |
38 | 39 |
40 |

41 |
42 | 43 |

44 |
45 | 46 |

47 |
48 | 49 | 50 | 51 |
52 | 53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/img/chromeDefaultFavicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/chromeDefaultFavicon.png -------------------------------------------------------------------------------- /src/img/chromeDefaultFaviconSml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/chromeDefaultFaviconSml.png -------------------------------------------------------------------------------- /src/img/chromeDevDefaultFavicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/chromeDevDefaultFavicon.png -------------------------------------------------------------------------------- /src/img/chromeDevDefaultFaviconSml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/chromeDevDefaultFaviconSml.png -------------------------------------------------------------------------------- /src/img/ic_suspendy_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/ic_suspendy_128x128.png -------------------------------------------------------------------------------- /src/img/ic_suspendy_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/ic_suspendy_16x16.png -------------------------------------------------------------------------------- /src/img/ic_suspendy_16x16_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/ic_suspendy_16x16_grey.png -------------------------------------------------------------------------------- /src/img/ic_suspendy_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/ic_suspendy_32x32.png -------------------------------------------------------------------------------- /src/img/ic_suspendy_32x32_grey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/ic_suspendy_32x32_grey.png -------------------------------------------------------------------------------- /src/img/ic_suspendy_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/ic_suspendy_48x48.png -------------------------------------------------------------------------------- /src/img/snoozy_tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/img/snoozy_tab_awake.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/img/suspendy-guy-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/suspendy-guy-alt.png -------------------------------------------------------------------------------- /src/img/suspendy-guy-oops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/suspendy-guy-oops.png -------------------------------------------------------------------------------- /src/img/suspendy-guy-uh-oh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/suspendy-guy-uh-oh.png -------------------------------------------------------------------------------- /src/img/suspendy-guy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dvalter/ff-thegreatsuspender/81bfe8395d024814af55db4af64a1a701a6f75eb/src/img/suspendy-guy.png -------------------------------------------------------------------------------- /src/js/about.js: -------------------------------------------------------------------------------- 1 | /* global chrome, XMLHttpRequest, gsStorage, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 13 | var versionEl = document.getElementById('aboutVersion'); 14 | versionEl.innerHTML = 'v' + chrome.runtime.getManifest().version; 15 | 16 | //hide incompatible sidebar items if in incognito mode 17 | if (chrome.extension.inIncognitoContext) { 18 | Array.prototype.forEach.call( 19 | document.getElementsByClassName('noIncognito'), 20 | function(el) { 21 | el.style.display = 'none'; 22 | } 23 | ); 24 | } 25 | }); 26 | })(this); 27 | -------------------------------------------------------------------------------- /src/js/broken.js: -------------------------------------------------------------------------------- 1 | /*global chrome */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | function init() { 13 | document 14 | .getElementById('restartExtension') 15 | .addEventListener('click', function() { 16 | chrome.runtime.reload(); 17 | }); 18 | document 19 | .getElementById('sessionManagementLink') 20 | .addEventListener('click', function() { 21 | chrome.tabs.create({ url: chrome.extension.getURL('history.html') }); 22 | }); 23 | } 24 | if (document.readyState !== 'loading') { 25 | init(); 26 | } else { 27 | document.addEventListener('DOMContentLoaded', function() { 28 | init(); 29 | }); 30 | } 31 | })(this); 32 | -------------------------------------------------------------------------------- /src/js/contentscript.js: -------------------------------------------------------------------------------- 1 | /*global chrome */ 2 | /* 3 | * The Great Suspender 4 | * Copyright (C) 2017 Dean Oemcke 5 | * Available under GNU GENERAL PUBLIC LICENSE v2 6 | * http://github.com/deanoemcke/thegreatsuspender 7 | * ლ(ಠ益ಠლ) 8 | */ 9 | (function() { 10 | 'use strict'; 11 | 12 | let isFormListenerInitialised = false; 13 | let isReceivingFormInput = false; 14 | let isIgnoreForms = false; 15 | let tempWhitelist = false; 16 | 17 | function formInputListener(e) { 18 | if (!isReceivingFormInput && !tempWhitelist) { 19 | if (event.keyCode >= 48 && event.keyCode <= 90 && event.target.tagName) { 20 | if ( 21 | event.target.tagName.toUpperCase() === 'INPUT' || 22 | event.target.tagName.toUpperCase() === 'TEXTAREA' || 23 | event.target.tagName.toUpperCase() === 'FORM' || 24 | event.target.isContentEditable === true || 25 | event.target.type === "application/pdf" 26 | ) { 27 | isReceivingFormInput = true; 28 | if (!isBackgroundConnectable()) { 29 | return false; 30 | } 31 | chrome.runtime.sendMessage(buildReportTabStatePayload()); 32 | } 33 | } 34 | } 35 | } 36 | 37 | function initFormInputListener() { 38 | if (isFormListenerInitialised) { 39 | return; 40 | } 41 | window.addEventListener('keydown', formInputListener); 42 | isFormListenerInitialised = true; 43 | } 44 | 45 | function init() { 46 | //listen for background events 47 | chrome.runtime.onMessage.addListener(function( 48 | request, 49 | sender, 50 | sendResponse 51 | ) { 52 | if (request.hasOwnProperty('action')) { 53 | if (request.action === 'requestInfo') { 54 | sendResponse(buildReportTabStatePayload()); 55 | return false; 56 | } 57 | } 58 | 59 | if (request.hasOwnProperty('scrollPos')) { 60 | if (request.scrollPos !== '' && request.scrollPos !== '0') { 61 | document.body.scrollTop = request.scrollPos; 62 | document.documentElement.scrollTop = request.scrollPos; 63 | } 64 | } 65 | if (request.hasOwnProperty('ignoreForms')) { 66 | isIgnoreForms = request.ignoreForms; 67 | if (isIgnoreForms) { 68 | initFormInputListener(); 69 | } 70 | isReceivingFormInput = isReceivingFormInput && isIgnoreForms; 71 | } 72 | if (request.hasOwnProperty('tempWhitelist')) { 73 | if (isReceivingFormInput && !request.tempWhitelist) { 74 | isReceivingFormInput = false; 75 | } 76 | tempWhitelist = request.tempWhitelist; 77 | } 78 | sendResponse(buildReportTabStatePayload()); 79 | return false; 80 | }); 81 | } 82 | 83 | function waitForRuntimeReady(retries) { 84 | retries = retries || 0; 85 | return new Promise(r => r(chrome.runtime)).then(chromeRuntime => { 86 | if (chromeRuntime) { 87 | return Promise.resolve(); 88 | } 89 | if (retries > 3) { 90 | return Promise.reject('Failed waiting for chrome.runtime'); 91 | } 92 | retries += 1; 93 | return new Promise(r => window.setTimeout(r, 500)).then(() => 94 | waitForRuntimeReady(retries) 95 | ); 96 | }); 97 | } 98 | 99 | function isBackgroundConnectable() { 100 | try { 101 | var port = chrome.runtime.connect(); 102 | if (port) { 103 | port.disconnect(); 104 | return true; 105 | } 106 | return false; 107 | } catch (e) { 108 | return false; 109 | } 110 | } 111 | 112 | function buildReportTabStatePayload() { 113 | return { 114 | action: 'reportTabState', 115 | status: 116 | isIgnoreForms && isReceivingFormInput 117 | ? 'formInput' 118 | : tempWhitelist 119 | ? 'tempWhitelist' 120 | : 'normal', 121 | scrollPos: 122 | document.body.scrollTop || document.documentElement.scrollTop || 0, 123 | }; 124 | } 125 | 126 | waitForRuntimeReady() 127 | .then(init) 128 | .catch(e => { 129 | console.error(e); 130 | setTimeout(() => { 131 | init(); 132 | }, 200); 133 | }); 134 | })(); 135 | -------------------------------------------------------------------------------- /src/js/debug.js: -------------------------------------------------------------------------------- 1 | /*global chrome, tgs, gsUtils, gsFavicon, gsStorage, gsChrome */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | var currentTabs = {}; 13 | 14 | function generateTabInfo(info) { 15 | // console.log(info.tabId, info); 16 | var timerStr = 17 | info && info.timerUp && info && info.timerUp !== '-' 18 | ? new Date(info.timerUp).toLocaleString() 19 | : '-'; 20 | var html = '', 21 | windowId = info && info.windowId ? info.windowId : '?', 22 | tabId = info && info.tabId ? info.tabId : '?', 23 | tabIndex = info && info.tab ? info.tab.index : '?', 24 | favicon = info && info.tab ? info.tab.favIconUrl : '', 25 | tabTitle = info && info.tab ? gsUtils.htmlEncode(info.tab.title) : '?', 26 | tabTimer = timerStr, 27 | tabStatus = info ? info.status : '?'; 28 | 29 | favicon = 30 | favicon && favicon.indexOf('data') === 0 31 | ? favicon 32 | : chrome.extension.getURL('img/chromeDefaultFavicon.png'); 33 | 34 | html += ''; 35 | html += '' + windowId + ''; 36 | html += '' + tabId + ''; 37 | html += '' + tabIndex + ''; 38 | html += ''; 39 | html += '' + tabTitle + ''; 40 | html += '' + tabTimer + ''; 41 | html += '' + tabStatus + ''; 42 | html += ''; 43 | 44 | return html; 45 | } 46 | 47 | async function fetchInfo() { 48 | const tabs = await gsChrome.tabsQuery(); 49 | const debugInfoPromises = []; 50 | for (const [i, curTab] of tabs.entries()) { 51 | currentTabs[tabs[i].id] = tabs[i]; 52 | debugInfoPromises.push( 53 | new Promise(r => 54 | tgs.getDebugInfo(curTab.id, o => { 55 | o.tab = curTab; 56 | r(o); 57 | }) 58 | ) 59 | ); 60 | } 61 | const debugInfos = await Promise.all(debugInfoPromises); 62 | for (const debugInfo of debugInfos) { 63 | var html, 64 | tableEl = document.getElementById('gsProfilerBody'); 65 | html = generateTabInfo(debugInfo); 66 | tableEl.innerHTML = tableEl.innerHTML + html; 67 | } 68 | } 69 | 70 | function addFlagHtml(elementId, getterFn, setterFn) { 71 | document.getElementById(elementId).innerHTML = getterFn(); 72 | document.getElementById(elementId).onclick = function(e) { 73 | const newVal = !getterFn(); 74 | setterFn(newVal); 75 | document.getElementById(elementId).innerHTML = newVal; 76 | }; 77 | } 78 | 79 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(async function() { 80 | await fetchInfo(); 81 | addFlagHtml( 82 | 'toggleDebugInfo', 83 | () => gsUtils.isDebugInfo(), 84 | newVal => gsUtils.setDebugInfo(newVal) 85 | ); 86 | addFlagHtml( 87 | 'toggleDebugError', 88 | () => gsUtils.isDebugError(), 89 | newVal => gsUtils.setDebugError(newVal) 90 | ); 91 | addFlagHtml( 92 | 'toggleDiscardInPlaceOfSuspend', 93 | () => gsStorage.getOption(gsStorage.DISCARD_IN_PLACE_OF_SUSPEND), 94 | newVal => { 95 | gsStorage.setOptionAndSync( 96 | gsStorage.DISCARD_IN_PLACE_OF_SUSPEND, 97 | newVal 98 | ); 99 | } 100 | ); 101 | addFlagHtml( 102 | 'toggleUseAlternateScreenCaptureLib', 103 | () => gsStorage.getOption(gsStorage.USE_ALT_SCREEN_CAPTURE_LIB), 104 | newVal => { 105 | gsStorage.setOptionAndSync( 106 | gsStorage.USE_ALT_SCREEN_CAPTURE_LIB, 107 | newVal 108 | ); 109 | } 110 | ); 111 | document.getElementById('claimSuspendedTabs').onclick = async function(e) { 112 | const tabs = await gsChrome.tabsQuery(); 113 | for (const tab of tabs) { 114 | if ( 115 | gsUtils.isSuspendedTab(tab, true) && 116 | tab.url.indexOf(chrome.runtime.id) < 0 117 | ) { 118 | const newUrl = tab.url.replace( 119 | gsUtils.getRootUrl(tab.url), 120 | chrome.runtime.id 121 | ); 122 | await gsChrome.tabsUpdate(tab.id, { url: newUrl }); 123 | } 124 | } 125 | }; 126 | 127 | var extensionsUrl = `about:devtools-toolbox?type=extension&id=${chrome.runtime.id}`; 128 | document 129 | .getElementById('backgroundPage') 130 | .setAttribute('href', extensionsUrl); 131 | document.getElementById('backgroundPage').onclick = function() { 132 | chrome.tabs.create({ url: extensionsUrl }); 133 | }; 134 | 135 | /* 136 | chrome.processes.onUpdatedWithMemory.addListener(function (processes) { 137 | chrome.tabs.query({}, function (tabs) { 138 | var html = ''; 139 | html += generateMemStats(processes); 140 | html += '
'; 141 | html += generateTabStats(tabs); 142 | document.getElementById('gsProfiler').innerHTML = html; 143 | }); 144 | }); 145 | */ 146 | }); 147 | 148 | window.onload = function() { 149 | document.getElementById('inspect-url').innerHTML = `about:devtools-toolbox?type=extension&id=${chrome.runtime.id}`; 150 | } 151 | })(this); 152 | -------------------------------------------------------------------------------- /src/js/gsChrome.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsUtils */ 2 | 'use strict'; 3 | // eslint-disable-next-line no-unused-vars 4 | var gsChrome = { 5 | cookiesGetAll: function() { 6 | return new Promise(resolve => { 7 | chrome.cookies.getAll({}, cookies => { 8 | if (chrome.runtime.lastError) { 9 | gsUtils.warning('chromeCookies', chrome.runtime.lastError); 10 | cookies = []; 11 | } 12 | resolve(cookies); 13 | }); 14 | }); 15 | }, 16 | cookiesRemove: function(url, name) { 17 | return new Promise(resolve => { 18 | if (!url || !name) { 19 | gsUtils.warning('chromeCookies', 'url or name not specified'); 20 | resolve(null); 21 | return; 22 | } 23 | chrome.cookies.remove({ url, name }, details => { 24 | if (chrome.runtime.lastError) { 25 | gsUtils.warning('chromeCookies', chrome.runtime.lastError); 26 | details = null; 27 | } 28 | resolve(details); 29 | }); 30 | }); 31 | }, 32 | 33 | tabsCreate: function(details) { 34 | return new Promise(resolve => { 35 | if ( 36 | !details || 37 | (typeof details !== 'string' && typeof details.url !== 'string') 38 | ) { 39 | gsUtils.warning('chromeTabs', 'url not specified'); 40 | resolve(null); 41 | return; 42 | } 43 | details = typeof details === 'string' ? { url: details } : details; 44 | chrome.tabs.create(details, tab => { 45 | if (chrome.runtime.lastError) { 46 | gsUtils.warning('chromeTabs', chrome.runtime.lastError); 47 | tab = null; 48 | } 49 | resolve(tab); 50 | }); 51 | }); 52 | }, 53 | tabsReload: function(tabId) { 54 | return new Promise(resolve => { 55 | if (!tabId) { 56 | gsUtils.warning('chromeTabs', 'tabId not specified'); 57 | resolve(false); 58 | return; 59 | } 60 | chrome.tabs.reload(tabId, () => { 61 | if (chrome.runtime.lastError) { 62 | gsUtils.warning('chromeTabs', chrome.runtime.lastError); 63 | resolve(false); 64 | return; 65 | } 66 | resolve(true); 67 | }); 68 | }); 69 | }, 70 | tabsUpdate: function(tabId, updateProperties) { 71 | return new Promise(resolve => { 72 | if (!tabId || !updateProperties) { 73 | gsUtils.warning( 74 | 'chromeTabs', 75 | 'tabId or updateProperties not specified' 76 | ); 77 | resolve(null); 78 | return; 79 | } 80 | chrome.tabs.update(tabId, updateProperties, tab => { 81 | if (chrome.runtime.lastError) { 82 | gsUtils.warning('chromeTabs', chrome.runtime.lastError); 83 | tab = null; 84 | } 85 | resolve(tab); 86 | }); 87 | }); 88 | }, 89 | tabsGet: function(tabId) { 90 | return new Promise(resolve => { 91 | if (!tabId) { 92 | gsUtils.warning('chromeTabs', 'tabId not specified'); 93 | resolve(null); 94 | return; 95 | } 96 | chrome.tabs.get(tabId, tab => { 97 | if (chrome.runtime.lastError) { 98 | gsUtils.warning('chromeTabs', chrome.runtime.lastError); 99 | tab = null; 100 | } 101 | resolve(tab); 102 | }); 103 | }); 104 | }, 105 | tabsQuery: function(queryInfo) { 106 | queryInfo = queryInfo || {}; 107 | return new Promise(resolve => { 108 | chrome.tabs.query(queryInfo, tabs => { 109 | if (chrome.runtime.lastError) { 110 | gsUtils.warning('chromeTabs', chrome.runtime.lastError); 111 | tabs = []; 112 | } 113 | resolve(tabs); 114 | }); 115 | }); 116 | }, 117 | tabsRemove: function(tabId) { 118 | return new Promise(resolve => { 119 | if (!tabId) { 120 | gsUtils.warning('chromeTabs', 'tabId not specified'); 121 | resolve(null); 122 | return; 123 | } 124 | chrome.tabs.remove(tabId, () => { 125 | if (chrome.runtime.lastError) { 126 | gsUtils.warning('chromeTabs', chrome.runtime.lastError); 127 | } 128 | resolve(); 129 | }); 130 | }); 131 | }, 132 | 133 | windowsGetLastFocused: function() { 134 | return new Promise(resolve => { 135 | chrome.windows.getLastFocused({}, window => { 136 | if (chrome.runtime.lastError) { 137 | gsUtils.warning('chromeWindows', chrome.runtime.lastError); 138 | window = null; 139 | } 140 | resolve(window); 141 | }); 142 | }); 143 | }, 144 | windowsGet: function(windowId) { 145 | return new Promise(resolve => { 146 | if (!windowId) { 147 | gsUtils.warning('chromeWindows', 'windowId not specified'); 148 | resolve(null); 149 | return; 150 | } 151 | chrome.windows.get(windowId, { populate: true }, window => { 152 | if (chrome.runtime.lastError) { 153 | gsUtils.warning('chromeWindows', chrome.runtime.lastError); 154 | window = null; 155 | } 156 | resolve(window); 157 | }); 158 | }); 159 | }, 160 | windowsGetAll: function() { 161 | return new Promise(resolve => { 162 | chrome.windows.getAll({ populate: true }, windows => { 163 | if (chrome.runtime.lastError) { 164 | gsUtils.warning('chromeWindows', chrome.runtime.lastError); 165 | windows = []; 166 | } 167 | resolve(windows); 168 | }); 169 | }); 170 | }, 171 | windowsCreate: function(createData) { 172 | createData = createData || {}; 173 | return new Promise(resolve => { 174 | chrome.windows.create(createData, window => { 175 | if (chrome.runtime.lastError) { 176 | gsUtils.warning('chromeWindows', chrome.runtime.lastError); 177 | window = null; 178 | } 179 | resolve(window); 180 | }); 181 | }); 182 | }, 183 | windowsUpdate: function(windowId, updateInfo) { 184 | return new Promise(resolve => { 185 | if (!windowId || !updateInfo) { 186 | gsUtils.warning('chromeTabs', 'windowId or updateInfo not specified'); 187 | resolve(null); 188 | return; 189 | } 190 | chrome.windows.update(windowId, updateInfo, window => { 191 | if (chrome.runtime.lastError) { 192 | gsUtils.warning('chromeWindows', chrome.runtime.lastError); 193 | window = null; 194 | } 195 | resolve(window); 196 | }); 197 | }); 198 | }, 199 | }; 200 | -------------------------------------------------------------------------------- /src/js/gsMessages.js: -------------------------------------------------------------------------------- 1 | /*global gsUtils, gsStorage */ 2 | // eslint-disable-next-line no-unused-vars 3 | var gsMessages = { 4 | INFO: 'info', 5 | WARNING: 'warning', 6 | ERROR: 'error', 7 | 8 | sendInitTabToContentScript( 9 | tabId, 10 | ignoreForms, 11 | tempWhitelist, 12 | scrollPos, 13 | callback 14 | ) { 15 | var payload = { 16 | ignoreForms: ignoreForms, 17 | tempWhitelist: tempWhitelist, 18 | }; 19 | if (scrollPos) { 20 | payload.scrollPos = scrollPos; 21 | } 22 | gsMessages.sendMessageToContentScript( 23 | tabId, 24 | payload, 25 | gsMessages.ERROR, 26 | callback 27 | ); 28 | }, 29 | 30 | sendUpdateToContentScriptOfTab: function(tab) { 31 | if ( 32 | gsUtils.isSpecialTab(tab) || 33 | gsUtils.isSuspendedTab(tab, true) || 34 | gsUtils.isDiscardedTab(tab) 35 | ) { 36 | return; 37 | } 38 | 39 | const ignoreForms = gsStorage.getOption(gsStorage.IGNORE_FORMS); 40 | gsMessages.sendMessageToContentScript( 41 | tab.id, 42 | { ignoreForms }, 43 | gsMessages.WARNING 44 | ); 45 | }, 46 | 47 | sendTemporaryWhitelistToContentScript: function(tabId, callback) { 48 | gsMessages.sendMessageToContentScript( 49 | tabId, 50 | { 51 | tempWhitelist: true, 52 | }, 53 | gsMessages.WARNING, 54 | callback 55 | ); 56 | }, 57 | 58 | sendUndoTemporaryWhitelistToContentScript: function(tabId, callback) { 59 | gsMessages.sendMessageToContentScript( 60 | tabId, 61 | { 62 | tempWhitelist: false, 63 | }, 64 | gsMessages.WARNING, 65 | callback 66 | ); 67 | }, 68 | 69 | sendRequestInfoToContentScript(tabId, callback) { 70 | gsMessages.sendMessageToContentScript( 71 | tabId, 72 | { 73 | action: 'requestInfo', 74 | }, 75 | gsMessages.WARNING, 76 | callback 77 | ); 78 | }, 79 | 80 | sendMessageToContentScript: function(tabId, message, severity, callback) { 81 | gsMessages.sendMessageToTab(tabId, message, severity, function( 82 | error, 83 | response 84 | ) { 85 | if (error) { 86 | if (callback) callback(error); 87 | } else { 88 | if (callback) callback(null, response); 89 | } 90 | }); 91 | }, 92 | 93 | sendPingToTab: function(tabId, callback) { 94 | gsMessages.sendMessageToTab( 95 | tabId, 96 | { 97 | action: 'ping', 98 | }, 99 | gsMessages.INFO, 100 | callback 101 | ); 102 | }, 103 | 104 | sendMessageToTab: function(tabId, message, severity, callback) { 105 | if (!tabId) { 106 | if (callback) callback('tabId not specified'); 107 | return; 108 | } 109 | var responseHandler = function(response) { 110 | gsUtils.log(tabId, 'response from tab', response); 111 | if (chrome.runtime.lastError) { 112 | if (callback) callback(chrome.runtime.lastError); 113 | } else { 114 | if (callback) callback(null, response); 115 | } 116 | }; 117 | 118 | message.tabId = tabId; 119 | try { 120 | gsUtils.log(tabId, 'send message to tab', message); 121 | chrome.tabs.sendMessage(tabId, message, { frameId: 0 }, responseHandler); 122 | } catch (e) { 123 | // gsUtils.error(tabId, e); 124 | chrome.tabs.sendMessage(tabId, message, responseHandler); 125 | } 126 | }, 127 | 128 | executeScriptOnTab: function(tabId, scriptPath, callback) { 129 | if (!tabId) { 130 | if (callback) callback('tabId not specified'); 131 | return; 132 | } 133 | gsUtils.log(`executing script ${scriptPath}`) 134 | 135 | var executing = browser.tabs.executeScript(tabId, { file: scriptPath }); 136 | 137 | if (executing) { 138 | executing.then(function(response) { 139 | if (callback) callback(null, response); 140 | }, function(error) { 141 | if (callback) callback(error); 142 | }) 143 | } 144 | }, 145 | 146 | executeCodeOnTab: function(tabId, codeString, callback) { 147 | if (!tabId) { 148 | if (callback) callback('tabId not specified'); 149 | return; 150 | } 151 | gsUtils.log(`executing code ${codeString}`) 152 | 153 | var executing = chrome.tabs.executeScript(tabId, { code: codeString }); 154 | 155 | if (executing) { 156 | 157 | executing.then(function(response) { 158 | if (callback) callback(null, response); 159 | }, function(error) { 160 | if (callback) callback(error); 161 | }) 162 | } 163 | }, 164 | }; 165 | -------------------------------------------------------------------------------- /src/js/gsTabDiscardManager.js: -------------------------------------------------------------------------------- 1 | /*global chrome, localStorage, tgs, gsUtils, gsChrome, GsTabQueue, gsStorage, gsTabSuspendManager */ 2 | // eslint-disable-next-line no-unused-vars 3 | var gsTabDiscardManager = (function() { 4 | 'use strict'; 5 | 6 | const DEFAULT_CONCURRENT_DISCARDS = 5; 7 | const DEFAULT_DISCARD_TIMEOUT = 5 * 1000; 8 | 9 | const QUEUE_ID = '_discardQueue'; 10 | 11 | let _discardQueue; 12 | 13 | function initAsPromised() { 14 | return new Promise(resolve => { 15 | const queueProps = { 16 | concurrentExecutors: DEFAULT_CONCURRENT_DISCARDS, 17 | jobTimeout: DEFAULT_DISCARD_TIMEOUT, 18 | executorFn: performDiscard, 19 | exceptionFn: handleDiscardException, 20 | }; 21 | _discardQueue = GsTabQueue(QUEUE_ID, queueProps); 22 | gsUtils.log(QUEUE_ID, 'init successful'); 23 | resolve(); 24 | }); 25 | } 26 | 27 | function queueTabForDiscard(tab, executionProps, processingDelay) { 28 | queueTabForDiscardAsPromise(tab, executionProps, processingDelay).catch( 29 | e => { 30 | gsUtils.log(tab.id, QUEUE_ID, e); 31 | } 32 | ); 33 | } 34 | 35 | function queueTabForDiscardAsPromise(tab, executionProps, processingDelay) { 36 | gsUtils.log(tab.id, QUEUE_ID, `Queueing tab for discarding.`); 37 | executionProps = executionProps || {}; 38 | return _discardQueue.queueTabAsPromise( 39 | tab, 40 | executionProps, 41 | processingDelay 42 | ); 43 | } 44 | 45 | function unqueueTabForDiscard(tab) { 46 | const removed = _discardQueue.unqueueTab(tab); 47 | if (removed) { 48 | gsUtils.log(tab.id, QUEUE_ID, 'Removed tab from discard queue'); 49 | } 50 | } 51 | 52 | // This is called remotely by the _discardQueue 53 | // So we must first re-fetch the tab in case it has changed 54 | async function performDiscard(tab, executionProps, resolve, reject, requeue) { 55 | let _tab = null; 56 | try { 57 | _tab = await gsChrome.tabsGet(tab.id); 58 | } catch (error) { 59 | // assume tab has been discarded 60 | } 61 | if (!_tab) { 62 | gsUtils.warning( 63 | tab.id, 64 | QUEUE_ID, 65 | `Failed to discard tab. Tab may have already been discarded or removed.` 66 | ); 67 | resolve(false); 68 | return; 69 | } 70 | tab = _tab; 71 | 72 | if (gsUtils.isSuspendedTab(tab) && tab.status === 'loading') { 73 | gsUtils.log(tab.id, QUEUE_ID, 'Tab is still loading'); 74 | requeue(); 75 | return; 76 | } 77 | if (tgs.isCurrentActiveTab(tab)) { 78 | const discardInPlaceOfSuspend = gsStorage.getOption( 79 | gsStorage.DISCARD_IN_PLACE_OF_SUSPEND 80 | ); 81 | if (!discardInPlaceOfSuspend) { 82 | gsUtils.log(tab.id, QUEUE_ID, 'Tab is active. Aborting discard.'); 83 | resolve(false); 84 | return; 85 | } 86 | } 87 | if (gsUtils.isDiscardedTab(tab)) { 88 | gsUtils.log(tab.id, QUEUE_ID, 'Tab already discarded'); 89 | resolve(false); 90 | return; 91 | } 92 | gsUtils.log(tab.id, QUEUE_ID, 'Forcing discarding of tab.'); 93 | browser.tabs.discard(tab.id).then(() => { 94 | resolve(true); 95 | }).catch((err) => { 96 | gsUtils.warning(tab.id, QUEUE_ID, err); 97 | resolve(false); 98 | }); 99 | } 100 | 101 | function handleDiscardException( 102 | tab, 103 | executionProps, 104 | exceptionType, 105 | resolve, 106 | reject, 107 | requeue 108 | ) { 109 | gsUtils.warning( 110 | tab.id, 111 | QUEUE_ID, 112 | `Failed to discard tab: ${exceptionType}` 113 | ); 114 | resolve(false); 115 | } 116 | 117 | async function handleDiscardedUnsuspendedTab(tab) { 118 | if ( 119 | gsUtils.shouldSuspendDiscardedTabs() && 120 | gsTabSuspendManager.checkTabEligibilityForSuspension(tab, 3) 121 | ) { 122 | tgs.setTabStatePropForTabId(tab.id, tgs.STATE_SUSPEND_REASON, 3); 123 | const suspendedUrl = gsUtils.generateSuspendedUrl(tab.url, tab.title, 0); 124 | gsUtils.log(tab.id, QUEUE_ID, 'Suspending discarded unsuspended tab'); 125 | 126 | // Note: This bypasses the suspension tab queue and also prevents screenshots from being taken 127 | await gsTabSuspendManager.executeTabSuspension(tab, suspendedUrl); 128 | return; 129 | } 130 | } 131 | 132 | return { 133 | initAsPromised, 134 | queueTabForDiscard, 135 | queueTabForDiscardAsPromise, 136 | unqueueTabForDiscard, 137 | handleDiscardedUnsuspendedTab, 138 | }; 139 | })(); 140 | -------------------------------------------------------------------------------- /src/js/gsTabQueue.js: -------------------------------------------------------------------------------- 1 | /*global gsUtils */ 2 | // eslint-disable-next-line no-unused-vars 3 | function GsTabQueue(queueId, queueProps) { 4 | return (function() { 5 | 'use strict'; 6 | 7 | const STATUS_QUEUED = 'queued'; 8 | const STATUS_IN_PROGRESS = 'inProgress'; 9 | const STATUS_SLEEPING = 'sleeping'; 10 | 11 | const EXCEPTION_TIMEOUT = 'timeout'; 12 | 13 | const DEFAULT_CONCURRENT_EXECUTORS = 1; 14 | const DEFAULT_JOB_TIMEOUT = 1000; 15 | const DEFAULT_PROCESSING_DELAY = 500; 16 | const DEFAULT_REQUEUE_DELAY = 5000; 17 | const PROCESSING_QUEUE_CHECK_INTERVAL = 50; 18 | 19 | const _queueProperties = { 20 | concurrentExecutors: DEFAULT_CONCURRENT_EXECUTORS, 21 | jobTimeout: DEFAULT_JOB_TIMEOUT, 22 | processingDelay: DEFAULT_PROCESSING_DELAY, 23 | executorFn: (tab, resolve, reject, requeue) => resolve(true), 24 | exceptionFn: (tab, resolve, reject, requeue) => resolve(false), 25 | }; 26 | const _tabDetailsByTabId = {}; 27 | const _queuedTabIds = []; 28 | let _processingQueueBufferTimer = null; 29 | let _queueId = queueId; 30 | 31 | setQueueProperties(queueProps); 32 | 33 | function setQueueProperties(queueProps) { 34 | for (const propName of Object.keys(queueProps)) { 35 | _queueProperties[propName] = queueProps[propName]; 36 | } 37 | if (!isValidInteger(_queueProperties.concurrentExecutors, 1)) { 38 | throw new Error( 39 | 'concurrentExecutors must be an integer greater than 0' 40 | ); 41 | } 42 | if (!isValidInteger(_queueProperties.jobTimeout, 1)) { 43 | throw new Error('jobTimeout must be an integer greater than 0'); 44 | } 45 | if (!isValidInteger(_queueProperties.processingDelay, 0)) { 46 | throw new Error('processingDelay must be an integer of at least 0'); 47 | } 48 | if (!(typeof _queueProperties.executorFn === 'function')) { 49 | throw new Error('executorFn must be a function'); 50 | } 51 | if (!(typeof _queueProperties.exceptionFn === 'function')) { 52 | throw new Error('executorFn must be a function'); 53 | } 54 | } 55 | 56 | function getQueueProperties() { 57 | return _queueProperties; 58 | } 59 | 60 | function isValidInteger(value, minimum) { 61 | return value !== null && !isNaN(Number(value) && value >= minimum); 62 | } 63 | 64 | function getTotalQueueSize() { 65 | return Object.keys(_tabDetailsByTabId).length; 66 | } 67 | 68 | function queueTabAsPromise(tab, executionProps, delay) { 69 | executionProps = executionProps || {}; 70 | let tabDetails = _tabDetailsByTabId[tab.id]; 71 | if (!tabDetails) { 72 | // gsUtils.log(tab.id, _queueId, 'Queueing new tab.'); 73 | tabDetails = { 74 | tab, 75 | executionProps, 76 | deferredPromise: createDeferredPromise(), 77 | status: STATUS_QUEUED, 78 | requeues: 0, 79 | }; 80 | addTabToQueue(tabDetails); 81 | } else { 82 | tabDetails.tab = tab; 83 | applyExecutionProps(tabDetails, executionProps); 84 | gsUtils.log(tab.id, _queueId, 'Tab already queued.'); 85 | } 86 | 87 | if (delay && isValidInteger(delay, 1)) { 88 | gsUtils.log(tab.id, _queueId, `Sleeping tab for ${delay}ms`); 89 | sleepTab(tabDetails, delay); 90 | } else { 91 | // If tab is already marked as sleeping then wake it up 92 | if (tabDetails.sleepTimer) { 93 | gsUtils.log(tab.id, _queueId, 'Removing tab from sleep'); 94 | clearTimeout(tabDetails.sleepTimer); 95 | delete tabDetails.sleepTimer; 96 | tabDetails.status = STATUS_QUEUED; 97 | } 98 | requestProcessQueue(0); 99 | } 100 | return tabDetails.deferredPromise; 101 | } 102 | 103 | function applyExecutionProps(tabDetails, executionProps) { 104 | executionProps = executionProps || {}; 105 | for (const prop in executionProps) { 106 | tabDetails.executionProps[prop] = executionProps[prop]; 107 | } 108 | } 109 | 110 | function unqueueTab(tab) { 111 | const tabDetails = _tabDetailsByTabId[tab.id]; 112 | if (tabDetails) { 113 | // gsUtils.log(tab.id, _queueId, 'Unqueueing tab.'); 114 | clearTimeout(tabDetails.timeoutTimer); 115 | removeTabFromQueue(tabDetails); 116 | rejectTabPromise(tabDetails, 'Queued tab job cancelled externally'); 117 | return true; 118 | } else { 119 | return false; 120 | } 121 | } 122 | 123 | function addTabToQueue(tabDetails) { 124 | const tab = tabDetails.tab; 125 | _tabDetailsByTabId[tab.id] = tabDetails; 126 | _queuedTabIds.push(tab.id); 127 | } 128 | 129 | function removeTabFromQueue(tabDetails) { 130 | const tab = tabDetails.tab; 131 | delete _tabDetailsByTabId[tab.id]; 132 | for (const [i, tabId] of _queuedTabIds.entries()) { 133 | if (tabId === tab.id) { 134 | _queuedTabIds.splice(i, 1); 135 | break; 136 | } 137 | } 138 | gsUtils.log(_queueId, `total queue size: ${_queuedTabIds.length}`); 139 | } 140 | 141 | // eslint-disable-next-line no-unused-vars 142 | function moveTabToEndOfQueue(tabDetails) { 143 | const tab = tabDetails.tab; 144 | for (const [i, tabId] of _queuedTabIds.entries()) { 145 | if (tabId === tab.id) { 146 | _queuedTabIds.push(_queuedTabIds.splice(i, 1)[0]); 147 | break; 148 | } 149 | } 150 | } 151 | 152 | function getQueuedTabDetails(tab) { 153 | return _tabDetailsByTabId[tab.id]; 154 | } 155 | 156 | function createDeferredPromise() { 157 | let res; 158 | let rej; 159 | const promise = new Promise((resolve, reject) => { 160 | res = resolve; 161 | rej = reject; 162 | }); 163 | promise.resolve = o => { 164 | res(o); 165 | return promise; 166 | }; 167 | promise.reject = o => { 168 | rej(o); 169 | return promise; 170 | }; 171 | return promise; 172 | } 173 | 174 | function requestProcessQueue(processingDelay) { 175 | setTimeout(() => { 176 | startProcessQueueBufferTimer(); 177 | }, processingDelay); 178 | } 179 | 180 | function startProcessQueueBufferTimer() { 181 | if (_processingQueueBufferTimer === null) { 182 | _processingQueueBufferTimer = setTimeout(() => { 183 | _processingQueueBufferTimer = null; 184 | processQueue(); 185 | }, PROCESSING_QUEUE_CHECK_INTERVAL); 186 | } 187 | } 188 | 189 | function processQueue() { 190 | let inProgressCount = 0; 191 | for (const tabId of _queuedTabIds) { 192 | const tabDetails = _tabDetailsByTabId[tabId]; 193 | if (tabDetails.status === STATUS_IN_PROGRESS) { 194 | inProgressCount += 1; 195 | } else if (tabDetails.status === STATUS_QUEUED) { 196 | processTab(tabDetails); 197 | inProgressCount += 1; 198 | } else if (tabDetails.status === STATUS_SLEEPING) { 199 | // ignore 200 | } 201 | if (inProgressCount >= _queueProperties.concurrentExecutors) { 202 | break; 203 | } 204 | } 205 | } 206 | 207 | function processTab(tabDetails) { 208 | tabDetails.status = STATUS_IN_PROGRESS; 209 | gsUtils.log( 210 | tabDetails.tab.id, 211 | _queueId, 212 | 'Executing executorFn for tab.' 213 | // tabDetails 214 | ); 215 | 216 | const _resolveTabPromise = r => resolveTabPromise(tabDetails, r); 217 | const _rejectTabPromise = e => rejectTabPromise(tabDetails, e); 218 | const _requeueTab = (requeueDelay, executionProps) => { 219 | requeueTab(tabDetails, requeueDelay, executionProps); 220 | }; 221 | 222 | // If timeout timer has not yet been initiated, then start it now 223 | if (!tabDetails.hasOwnProperty('timeoutTimer')) { 224 | tabDetails.timeoutTimer = setTimeout(() => { 225 | gsUtils.log(tabDetails.tab.id, _queueId, 'Tab job timed out'); 226 | _queueProperties.exceptionFn( 227 | tabDetails.tab, 228 | tabDetails.executionProps, 229 | EXCEPTION_TIMEOUT, 230 | _resolveTabPromise, 231 | _rejectTabPromise, 232 | _requeueTab 233 | ); //async. unhandled promise 234 | }, _queueProperties.jobTimeout); 235 | } 236 | 237 | _queueProperties.executorFn( 238 | tabDetails.tab, 239 | tabDetails.executionProps, 240 | _resolveTabPromise, 241 | _rejectTabPromise, 242 | _requeueTab 243 | ); //async. unhandled promise 244 | } 245 | 246 | function resolveTabPromise(tabDetails, result) { 247 | if (!_tabDetailsByTabId[tabDetails.tab.id]) { 248 | return; 249 | } 250 | gsUtils.log( 251 | tabDetails.tab.id, 252 | _queueId, 253 | 'Queued tab resolved. Result: ', 254 | result 255 | ); 256 | clearTimeout(tabDetails.timeoutTimer); 257 | removeTabFromQueue(tabDetails); 258 | tabDetails.deferredPromise.resolve(result); 259 | requestProcessQueue(_queueProperties.processingDelay); 260 | } 261 | 262 | function rejectTabPromise(tabDetails, error) { 263 | if (!_tabDetailsByTabId[tabDetails.tab.id]) { 264 | return; 265 | } 266 | gsUtils.log( 267 | tabDetails.tab.id, 268 | _queueId, 269 | 'Queued tab rejected. Error: ', 270 | error 271 | ); 272 | clearTimeout(tabDetails.timeoutTimer); 273 | removeTabFromQueue(tabDetails); 274 | tabDetails.deferredPromise.reject(error); 275 | requestProcessQueue(_queueProperties.processingDelay); 276 | } 277 | 278 | function requeueTab(tabDetails, requeueDelay, executionProps) { 279 | requeueDelay = requeueDelay || DEFAULT_REQUEUE_DELAY; 280 | if (executionProps) { 281 | applyExecutionProps(tabDetails, executionProps); 282 | } 283 | tabDetails.requeues += 1; 284 | gsUtils.log( 285 | tabDetails.tab.id, 286 | _queueId, 287 | `Requeueing tab. Requeues: ${tabDetails.requeues}` 288 | ); 289 | // moveTabToEndOfQueue(tabDetails); 290 | sleepTab(tabDetails, requeueDelay); 291 | requestProcessQueue(_queueProperties.processingDelay); 292 | } 293 | 294 | function sleepTab(tabDetails, delay) { 295 | tabDetails.status = STATUS_SLEEPING; 296 | if (tabDetails.sleepTimer) { 297 | clearTimeout(tabDetails.sleepTimer); 298 | } 299 | tabDetails.sleepTimer = window.setTimeout(() => { 300 | delete tabDetails.sleepTimer; 301 | tabDetails.status = STATUS_QUEUED; 302 | requestProcessQueue(0); 303 | }, delay); 304 | } 305 | 306 | return { 307 | EXCEPTION_TIMEOUT, 308 | setQueueProperties, 309 | getQueueProperties, 310 | getTotalQueueSize, 311 | queueTabAsPromise, 312 | unqueueTab, 313 | getQueuedTabDetails, 314 | }; 315 | })(); 316 | } 317 | -------------------------------------------------------------------------------- /src/js/history.js: -------------------------------------------------------------------------------- 1 | /*global chrome, historyItems, historyUtils, gsSession, gsIndexedDb, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | async function reloadTabs(sessionId, windowId, openTabsAsSuspended) { 13 | const session = await gsIndexedDb.fetchSessionBySessionId(sessionId); 14 | if (!session || !session.windows) { 15 | return; 16 | } 17 | 18 | gsUtils.removeInternalUrlsFromSession(session); 19 | 20 | //if loading a specific window 21 | let sessionWindows = []; 22 | if (windowId) { 23 | sessionWindows.push(gsUtils.getWindowFromSession(windowId, session)); 24 | //else load all windows from session 25 | } else { 26 | sessionWindows = session.windows; 27 | } 28 | 29 | for (let sessionWindow of sessionWindows) { 30 | const suspendMode = openTabsAsSuspended ? 1 : 2; 31 | await gsSession.restoreSessionWindow(sessionWindow, null, suspendMode); 32 | } 33 | } 34 | 35 | function deleteSession(sessionId) { 36 | var result = window.confirm( 37 | chrome.i18n.getMessage('js_history_confirm_delete') 38 | ); 39 | if (result) { 40 | gsIndexedDb.removeSessionFromHistory(sessionId).then(function() { 41 | window.location.reload(); 42 | }); 43 | } 44 | } 45 | 46 | function removeTab(element, sessionId, windowId, tabId) { 47 | var sessionEl, newSessionEl; 48 | 49 | gsIndexedDb 50 | .removeTabFromSessionHistory(sessionId, windowId, tabId) 51 | .then(function(session) { 52 | gsUtils.removeInternalUrlsFromSession(session); 53 | //if we have a valid session returned 54 | if (session) { 55 | sessionEl = element.parentElement.parentElement; 56 | newSessionEl = createSessionElement(session); 57 | sessionEl.parentElement.replaceChild(newSessionEl, sessionEl); 58 | toggleSession(newSessionEl, session.sessionId); //async. unhandled promise 59 | 60 | //otherwise assume it was the last tab in session and session has been removed 61 | } else { 62 | window.location.reload(); 63 | } 64 | }); 65 | } 66 | 67 | async function toggleSession(element, sessionId) { 68 | var sessionContentsEl = element.getElementsByClassName( 69 | 'sessionContents' 70 | )[0]; 71 | var sessionIcon = element.getElementsByClassName('sessionIcon')[0]; 72 | if (sessionIcon.classList.contains('icon-plus-squared-alt')) { 73 | sessionIcon.classList.remove('icon-plus-squared-alt'); 74 | sessionIcon.classList.add('icon-minus-squared-alt'); 75 | } else { 76 | sessionIcon.classList.remove('icon-minus-squared-alt'); 77 | sessionIcon.classList.add('icon-plus-squared-alt'); 78 | } 79 | 80 | //if toggled on already, then toggle off 81 | if (sessionContentsEl.childElementCount > 0) { 82 | sessionContentsEl.innerHTML = ''; 83 | return; 84 | } 85 | 86 | gsIndexedDb 87 | .fetchSessionBySessionId(sessionId) 88 | .then(async function(curSession) { 89 | if (!curSession || !curSession.windows) { 90 | return; 91 | } 92 | gsUtils.removeInternalUrlsFromSession(curSession); 93 | 94 | for (const [i, curWindow] of curSession.windows.entries()) { 95 | curWindow.sessionId = curSession.sessionId; 96 | sessionContentsEl.appendChild( 97 | createWindowElement(curSession, curWindow, i) 98 | ); 99 | 100 | const tabPromises = []; 101 | for (const curTab of curWindow.tabs) { 102 | curTab.windowId = curWindow.id; 103 | curTab.sessionId = curSession.sessionId; 104 | curTab.title = gsUtils.getCleanTabTitle(curTab); 105 | if (gsUtils.isSuspendedTab(curTab)) { 106 | curTab.url = gsUtils.getOriginalUrl(curTab.url); 107 | } 108 | tabPromises.push(createTabElement(curSession, curWindow, curTab)); 109 | } 110 | const tabEls = await Promise.all(tabPromises); 111 | for (const tabEl of tabEls) { 112 | sessionContentsEl.appendChild(tabEl); 113 | } 114 | } 115 | }); 116 | } 117 | 118 | function addClickListenerToElement(element, func) { 119 | if (element) { 120 | element.onclick = func; 121 | } 122 | } 123 | 124 | function createSessionElement(session) { 125 | var sessionEl = historyItems.createSessionHtml(session, true); 126 | 127 | addClickListenerToElement( 128 | sessionEl.getElementsByClassName('sessionIcon')[0], 129 | function() { 130 | toggleSession(sessionEl, session.sessionId); //async. unhandled promise 131 | } 132 | ); 133 | addClickListenerToElement( 134 | sessionEl.getElementsByClassName('sessionLink')[0], 135 | function() { 136 | toggleSession(sessionEl, session.sessionId); //async. unhandled promise 137 | } 138 | ); 139 | addClickListenerToElement( 140 | sessionEl.getElementsByClassName('exportLink')[0], 141 | function() { 142 | historyUtils.exportSessionWithId(session.sessionId); 143 | } 144 | ); 145 | addClickListenerToElement( 146 | sessionEl.getElementsByClassName('resuspendLink')[0], 147 | function() { 148 | reloadTabs(session.sessionId, null, true); // async 149 | } 150 | ); 151 | addClickListenerToElement( 152 | sessionEl.getElementsByClassName('reloadLink')[0], 153 | function() { 154 | reloadTabs(session.sessionId, null, false); // async 155 | } 156 | ); 157 | addClickListenerToElement( 158 | sessionEl.getElementsByClassName('saveLink')[0], 159 | function() { 160 | historyUtils.saveSession(session.sessionId); 161 | } 162 | ); 163 | addClickListenerToElement( 164 | sessionEl.getElementsByClassName('deleteLink')[0], 165 | function() { 166 | deleteSession(session.sessionId); 167 | } 168 | ); 169 | return sessionEl; 170 | } 171 | 172 | function createWindowElement(session, window, index) { 173 | var allowReload = session.sessionId !== gsSession.getSessionId(); 174 | var windowEl = historyItems.createWindowHtml(window, index, allowReload); 175 | 176 | addClickListenerToElement( 177 | windowEl.getElementsByClassName('resuspendLink')[0], 178 | function() { 179 | reloadTabs(session.sessionId, window.id, true); // async 180 | } 181 | ); 182 | addClickListenerToElement( 183 | windowEl.getElementsByClassName('reloadLink')[0], 184 | function() { 185 | reloadTabs(session.sessionId, window.id, false); // async 186 | } 187 | ); 188 | return windowEl; 189 | } 190 | 191 | async function createTabElement(session, window, tab) { 192 | var allowDelete = session.sessionId !== gsSession.getSessionId(); 193 | var tabEl = await historyItems.createTabHtml(tab, allowDelete); 194 | 195 | addClickListenerToElement( 196 | tabEl.getElementsByClassName('removeLink')[0], 197 | function() { 198 | removeTab(tabEl, session.sessionId, window.id, tab.id); 199 | } 200 | ); 201 | return tabEl; 202 | } 203 | 204 | function render() { 205 | var currentDiv = document.getElementById('currentSessions'), 206 | sessionsDiv = document.getElementById('recoverySessions'), 207 | historyDiv = document.getElementById('historySessions'), 208 | importSessionEl = document.getElementById('importSession'), 209 | importSessionActionEl = document.getElementById('importSessionAction'), 210 | firstSession = true; 211 | 212 | currentDiv.innerHTML = ''; 213 | sessionsDiv.innerHTML = ''; 214 | historyDiv.innerHTML = ''; 215 | 216 | gsIndexedDb.fetchCurrentSessions().then(function(currentSessions) { 217 | currentSessions.forEach(function(session, index) { 218 | gsUtils.removeInternalUrlsFromSession(session); 219 | var sessionEl = createSessionElement(session); 220 | if (firstSession) { 221 | currentDiv.appendChild(sessionEl); 222 | firstSession = false; 223 | } else { 224 | sessionsDiv.appendChild(sessionEl); 225 | } 226 | }); 227 | }); 228 | 229 | gsIndexedDb.fetchSavedSessions().then(function(savedSessions) { 230 | savedSessions.forEach(function(session, index) { 231 | gsUtils.removeInternalUrlsFromSession(session); 232 | var sessionEl = createSessionElement(session); 233 | historyDiv.appendChild(sessionEl); 234 | }); 235 | }); 236 | 237 | importSessionActionEl.addEventListener( 238 | 'change', 239 | historyUtils.importSession, 240 | false 241 | ); 242 | importSessionEl.onclick = function() { 243 | importSessionActionEl.click(); 244 | }; 245 | 246 | //hide incompatible sidebar items if in incognito mode 247 | if (chrome.extension.inIncognitoContext) { 248 | Array.prototype.forEach.call( 249 | document.getElementsByClassName('noIncognito'), 250 | function(el) { 251 | el.style.display = 'none'; 252 | } 253 | ); 254 | } 255 | } 256 | 257 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 258 | render(); 259 | }); 260 | })(this); 261 | -------------------------------------------------------------------------------- /src/js/historyItems.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsSession, gsUtils, gsFavicon */ 2 | // eslint-disable-next-line no-unused-vars 3 | var historyItems = (function(global) { 4 | 'use strict'; 5 | 6 | if ( 7 | !chrome.extension.getBackgroundPage() || 8 | !chrome.extension.getBackgroundPage().tgs 9 | ) { 10 | return; 11 | } 12 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 13 | 14 | function createSessionHtml(session, showLinks) { 15 | session.windows = session.windows || []; 16 | 17 | var sessionType = 18 | session.sessionId === gsSession.getSessionId() 19 | ? 'current' 20 | : session.name 21 | ? 'saved' 22 | : 'recent', 23 | sessionContainer, 24 | sessionTitle, 25 | sessionSave, 26 | sessionDelete, 27 | sessionExport, 28 | sessionDiv, 29 | sessionIcon, 30 | windowResuspend, 31 | windowReload, 32 | titleText, 33 | winCnt = session.windows.length, 34 | tabCnt = session.windows.reduce(function(a, b) { 35 | return a + b.tabs.length; 36 | }, 0); 37 | 38 | if (sessionType === 'saved') { 39 | titleText = session.name; 40 | } else { 41 | titleText = gsUtils.getHumanDate(session.date); 42 | } 43 | titleText += 44 | '  (' + 45 | winCnt + 46 | pluralise( 47 | ' ' + chrome.i18n.getMessage('js_history_window').toLowerCase(), 48 | winCnt 49 | ) + 50 | ', ' + 51 | tabCnt + 52 | pluralise( 53 | ' ' + chrome.i18n.getMessage('js_history_tab').toLowerCase(), 54 | tabCnt 55 | ) + 56 | ')'; 57 | 58 | sessionIcon = createEl('i', { 59 | class: 'sessionIcon icon icon-plus-squared-alt', 60 | }); 61 | 62 | sessionDiv = createEl('div', { 63 | class: 'sessionContents', 64 | }); 65 | 66 | sessionTitle = createEl('span', { 67 | class: 'sessionLink', 68 | }); 69 | sessionTitle.innerHTML = titleText; 70 | 71 | sessionSave = createEl( 72 | 'a', 73 | { 74 | class: 'groupLink saveLink', 75 | href: '#', 76 | }, 77 | chrome.i18n.getMessage('js_history_save') 78 | ); 79 | 80 | sessionDelete = createEl( 81 | 'a', 82 | { 83 | class: 'groupLink deleteLink', 84 | href: '#', 85 | }, 86 | chrome.i18n.getMessage('js_history_delete') 87 | ); 88 | 89 | windowResuspend = createEl( 90 | 'a', 91 | { 92 | class: 'groupLink resuspendLink', 93 | href: '#', 94 | }, 95 | chrome.i18n.getMessage('js_history_resuspend') 96 | ); 97 | 98 | windowReload = createEl( 99 | 'a', 100 | { 101 | class: 'groupLink reloadLink', 102 | href: '#', 103 | }, 104 | chrome.i18n.getMessage('js_history_reload') 105 | ); 106 | 107 | sessionExport = createEl( 108 | 'a', 109 | { 110 | class: 'groupLink exportLink', 111 | href: '#', 112 | }, 113 | chrome.i18n.getMessage('js_history_export') 114 | ); 115 | 116 | sessionContainer = createEl('div', { 117 | class: 'sessionContainer', 118 | }); 119 | sessionContainer.appendChild(sessionIcon); 120 | sessionContainer.appendChild(sessionTitle); 121 | if (showLinks && sessionType !== 'current') { 122 | sessionContainer.appendChild(windowResuspend); 123 | sessionContainer.appendChild(windowReload); 124 | } 125 | if (showLinks) { 126 | sessionContainer.appendChild(sessionExport); 127 | } 128 | if (showLinks && sessionType !== 'saved') { 129 | sessionContainer.appendChild(sessionSave); 130 | } 131 | if (showLinks && sessionType !== 'current') { 132 | sessionContainer.appendChild(sessionDelete); 133 | } 134 | 135 | sessionContainer.appendChild(sessionDiv); 136 | 137 | return sessionContainer; 138 | } 139 | 140 | function createWindowHtml(window, index, showLinks) { 141 | var groupHeading, windowContainer, groupUnsuspendCurrent, groupUnsuspendNew; 142 | 143 | groupHeading = createEl('div', { 144 | class: 'windowContainer', 145 | }); 146 | 147 | var windowString = chrome.i18n.getMessage('js_history_window'); 148 | windowContainer = createEl( 149 | 'span', 150 | {}, 151 | windowString + ' ' + (index + 1) + ':\u00A0' 152 | ); 153 | 154 | groupUnsuspendCurrent = createEl( 155 | 'a', 156 | { 157 | class: 'groupLink resuspendLink ', 158 | href: '#', 159 | }, 160 | chrome.i18n.getMessage('js_history_resuspend') 161 | ); 162 | 163 | groupUnsuspendNew = createEl( 164 | 'a', 165 | { 166 | class: 'groupLink reloadLink', 167 | href: '#', 168 | }, 169 | chrome.i18n.getMessage('js_history_reload') 170 | ); 171 | 172 | groupHeading.appendChild(windowContainer); 173 | if (showLinks) { 174 | groupHeading.appendChild(groupUnsuspendCurrent); 175 | groupHeading.appendChild(groupUnsuspendNew); 176 | } 177 | 178 | return groupHeading; 179 | } 180 | 181 | async function createTabHtml(tab, showLinks) { 182 | var linksSpan, listImg, listLink, listHover; 183 | 184 | if (tab.sessionId) { 185 | linksSpan = createEl('div', { 186 | class: 'tabContainer', 187 | 'data-tabId': tab.id || tab.url, 188 | 'data-url': tab.url, 189 | }); 190 | } else { 191 | linksSpan = createEl('div', { 192 | class: 'tabContainer', 193 | 'data-url': tab.url, 194 | }); 195 | } 196 | 197 | listHover = createEl( 198 | 'span', 199 | { 200 | class: 'itemHover removeLink', 201 | }, 202 | '\u2716' 203 | ); 204 | 205 | const faviconMeta = await gsFavicon.getFaviconMetaData(tab); 206 | const favIconUrl = faviconMeta.normalisedDataUrl; 207 | listImg = createEl('img', { 208 | src: favIconUrl, 209 | height: '16px', 210 | width: '16px', 211 | }); 212 | 213 | listLink = createEl( 214 | 'a', 215 | { 216 | class: 'historyLink', 217 | href: tab.url, 218 | target: '_blank', 219 | }, 220 | tab.title && tab.title.length > 1 ? tab.title : tab.url 221 | ); 222 | 223 | if (showLinks) { 224 | linksSpan.appendChild(listHover); 225 | } 226 | linksSpan.appendChild(listImg); 227 | linksSpan.appendChild(listLink); 228 | linksSpan.appendChild(createEl('br')); 229 | 230 | return linksSpan; 231 | } 232 | 233 | function createEl(elType, attributes, text) { 234 | var el = document.createElement(elType); 235 | attributes = attributes || {}; 236 | el = setElAttributes(el, attributes); 237 | el.innerHTML = gsUtils.htmlEncode(text || ''); 238 | return el; 239 | } 240 | function setElAttributes(el, attributes) { 241 | for (var key in attributes) { 242 | if (attributes.hasOwnProperty(key)) { 243 | el.setAttribute(key, attributes[key]); 244 | } 245 | } 246 | return el; 247 | } 248 | 249 | function pluralise(text, count) { 250 | return ( 251 | text + (count > 1 ? chrome.i18n.getMessage('js_history_plural') : '') 252 | ); 253 | } 254 | 255 | return { 256 | createSessionHtml: createSessionHtml, 257 | createWindowHtml: createWindowHtml, 258 | createTabHtml: createTabHtml, 259 | }; 260 | })(this); 261 | -------------------------------------------------------------------------------- /src/js/historyUtils.js: -------------------------------------------------------------------------------- 1 | /* global chrome, gsIndexedDb, gsUtils */ 2 | // eslint-disable-next-line no-unused-vars 3 | var historyUtils = (function(global) { 4 | 'use strict'; 5 | 6 | if ( 7 | !chrome.extension.getBackgroundPage() || 8 | !chrome.extension.getBackgroundPage().tgs 9 | ) { 10 | return; 11 | } 12 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 13 | 14 | var noop = function() {}; 15 | 16 | function importSession(e) { 17 | var f = e.target.files[0]; 18 | if (f) { 19 | var r = new FileReader(); 20 | r.onload = function(e) { 21 | var contents = e.target.result; 22 | if (f.type !== 'text/plain') { 23 | alert(chrome.i18n.getMessage('js_history_import_fail')); 24 | } else { 25 | handleImport(f.name, contents).then(function() { 26 | window.location.reload(); 27 | }); 28 | } 29 | }; 30 | r.readAsText(f); 31 | } else { 32 | alert(chrome.i18n.getMessage('js_history_import_fail')); 33 | } 34 | } 35 | 36 | async function handleImport(sessionName, textContents) { 37 | sessionName = window.prompt( 38 | chrome.i18n.getMessage('js_history_enter_name_for_session'), 39 | sessionName 40 | ); 41 | if (sessionName) { 42 | const shouldSave = await new Promise(resolve => { 43 | validateNewSessionName(sessionName, function(result) { 44 | resolve(result); 45 | }); 46 | }); 47 | if (!shouldSave) { 48 | return; 49 | } 50 | 51 | var sessionId = '_' + gsUtils.generateHashCode(sessionName); 52 | var windows = []; 53 | 54 | var createNextWindow = function() { 55 | return { 56 | id: sessionId + '_' + windows.length, 57 | tabs: [], 58 | }; 59 | }; 60 | var curWindow = createNextWindow(); 61 | 62 | for (const line of textContents.split('\n')) { 63 | if (typeof line !== 'string') { 64 | continue; 65 | } 66 | if (line === '') { 67 | if (curWindow.tabs.length > 0) { 68 | windows.push(curWindow); 69 | curWindow = createNextWindow(); 70 | } 71 | continue; 72 | } 73 | if (line.indexOf('://') < 0) { 74 | continue; 75 | } 76 | const tabInfo = { 77 | windowId: curWindow.id, 78 | sessionId: sessionId, 79 | id: curWindow.id + '_' + curWindow.tabs.length, 80 | url: line, 81 | title: line, 82 | index: curWindow.tabs.length, 83 | pinned: false, 84 | }; 85 | const savedTabInfo = await gsIndexedDb.fetchTabInfo(line); 86 | if (savedTabInfo) { 87 | tabInfo.title = savedTabInfo.title; 88 | tabInfo.favIconUrl = savedTabInfo.favIconUrl; 89 | } 90 | curWindow.tabs.push(tabInfo); 91 | } 92 | if (curWindow.tabs.length > 0) { 93 | windows.push(curWindow); 94 | } 95 | 96 | var session = { 97 | name: sessionName, 98 | sessionId: sessionId, 99 | windows: windows, 100 | date: new Date().toISOString(), 101 | }; 102 | await gsIndexedDb.updateSession(session); 103 | } 104 | } 105 | 106 | function exportSessionWithId(sessionId, callback) { 107 | callback = typeof callback !== 'function' ? noop : callback; 108 | 109 | gsIndexedDb.fetchSessionBySessionId(sessionId).then(function(session) { 110 | if (!session || !session.windows) { 111 | callback(); 112 | } else { 113 | exportSession(session, callback); 114 | } 115 | }); 116 | } 117 | 118 | function exportSession(session, callback) { 119 | let sessionString = ''; 120 | 121 | session.windows.forEach(function(curWindow, index) { 122 | curWindow.tabs.forEach(function(curTab, tabIndex) { 123 | if (gsUtils.isSuspendedTab(curTab)) { 124 | sessionString += gsUtils.getOriginalUrl(curTab.url) + '\n'; 125 | } else { 126 | sessionString += curTab.url + '\n'; 127 | } 128 | }); 129 | //add an extra newline to separate windows 130 | sessionString += '\n'; 131 | }); 132 | 133 | const blob = new Blob([sessionString], { type: 'text/plain' }); 134 | const blobUrl = URL.createObjectURL(blob); 135 | const link = document.createElement('a'); 136 | link.setAttribute('href', blobUrl); 137 | link.setAttribute('download', 'session.txt'); 138 | link.click(); 139 | 140 | callback(); 141 | } 142 | 143 | function validateNewSessionName(sessionName, callback) { 144 | gsIndexedDb.fetchSavedSessions().then(function(savedSessions) { 145 | var nameExists = savedSessions.some(function(savedSession, index) { 146 | return savedSession.name === sessionName; 147 | }); 148 | if (nameExists) { 149 | var overwrite = window.confirm( 150 | chrome.i18n.getMessage('js_history_confirm_session_overwrite') 151 | ); 152 | if (!overwrite) { 153 | callback(false); 154 | return; 155 | } 156 | } 157 | callback(true); 158 | }); 159 | } 160 | 161 | function saveSession(sessionId) { 162 | gsIndexedDb.fetchSessionBySessionId(sessionId).then(function(session) { 163 | if (!session) { 164 | gsUtils.warning( 165 | 'historyUtils', 166 | 'Could not find session with sessionId: ' + 167 | sessionId + 168 | '. Save aborted' 169 | ); 170 | return; 171 | } 172 | var sessionName = window.prompt( 173 | chrome.i18n.getMessage('js_history_enter_name_for_session') 174 | ); 175 | if (sessionName) { 176 | historyUtils.validateNewSessionName(sessionName, function(shouldSave) { 177 | if (shouldSave) { 178 | session.name = sessionName; 179 | gsIndexedDb.addToSavedSessions(session).then(function() { 180 | window.location.reload(); 181 | }); 182 | } 183 | }); 184 | } 185 | }); 186 | } 187 | 188 | return { 189 | importSession, 190 | exportSession, 191 | exportSessionWithId, 192 | validateNewSessionName, 193 | saveSession, 194 | }; 195 | })(this); 196 | -------------------------------------------------------------------------------- /src/js/notice.js: -------------------------------------------------------------------------------- 1 | /*global chrome, tgs, gsStorage, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 13 | var notice = tgs.requestNotice(); 14 | if ( 15 | notice && 16 | notice.hasOwnProperty('text') && 17 | notice.hasOwnProperty('version') 18 | ) { 19 | var noticeContentEl = document.getElementById('gsNotice'); 20 | noticeContentEl.innerHTML = notice.text; 21 | //update local notice version 22 | gsStorage.setNoticeVersion(notice.version); 23 | } 24 | 25 | //clear notice (to prevent it showing again) 26 | tgs.clearNotice(); 27 | }); 28 | })(this); 29 | -------------------------------------------------------------------------------- /src/js/options.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsStorage, gsChrome, gsUtils */ 2 | (function(global) { 3 | try { 4 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 5 | } catch (e) { 6 | window.setTimeout(() => window.location.reload(), 1000); 7 | return; 8 | } 9 | 10 | var elementPrefMap = { 11 | preview: gsStorage.SCREEN_CAPTURE, 12 | forceScreenCapture: gsStorage.SCREEN_CAPTURE_FORCE, 13 | suspendInPlaceOfDiscard: gsStorage.SUSPEND_IN_PLACE_OF_DISCARD, 14 | discardInPlaceOfSuspend: gsStorage.DISCARD_IN_PLACE_OF_SUSPEND, 15 | onlineCheck: gsStorage.IGNORE_WHEN_OFFLINE, 16 | batteryCheck: gsStorage.IGNORE_WHEN_CHARGING, 17 | unsuspendOnFocus: gsStorage.UNSUSPEND_ON_FOCUS, 18 | discardAfterSuspend: gsStorage.DISCARD_AFTER_SUSPEND, 19 | dontSuspendPinned: gsStorage.IGNORE_PINNED, 20 | dontSuspendForms: gsStorage.IGNORE_FORMS, 21 | dontSuspendAudio: gsStorage.IGNORE_AUDIO, 22 | dontSuspendActiveTabs: gsStorage.IGNORE_ACTIVE_TABS, 23 | ignoreCache: gsStorage.IGNORE_CACHE, 24 | addContextMenu: gsStorage.ADD_CONTEXT, 25 | syncSettings: gsStorage.SYNC_SETTINGS, 26 | timeToSuspend: gsStorage.SUSPEND_TIME, 27 | theme: gsStorage.THEME, 28 | whitelist: gsStorage.WHITELIST, 29 | }; 30 | 31 | var hideWhenDiscardSet = [ 32 | 'previewContainer', 33 | 'suspendedOptionsContainer', 34 | 'suspendInPlaceOfDiscard', 35 | ] 36 | 37 | function selectComboBox(element, key) { 38 | console.log("conbobox", element, key); 39 | var i, child; 40 | 41 | for (i = 0; i < element.children.length; i += 1) { 42 | child = element.children[i]; 43 | if (child.value === key) { 44 | child.selected = 'true'; 45 | break; 46 | } 47 | } 48 | } 49 | 50 | //populate settings from synced storage 51 | function initSettings() { 52 | var optionEls = document.getElementsByClassName('option'), 53 | pref, 54 | element, 55 | i; 56 | 57 | for (i = 0; i < optionEls.length; i++) { 58 | element = optionEls[i]; 59 | pref = elementPrefMap[element.id]; 60 | populateOption(element, gsStorage.getOption(pref)); 61 | } 62 | 63 | setForceScreenCaptureVisibility( 64 | gsStorage.getOption(gsStorage.SCREEN_CAPTURE) !== '0' 65 | ); 66 | setAutoSuspendOptionsVisibility( 67 | parseFloat(gsStorage.getOption(gsStorage.SUSPEND_TIME)) > 0 68 | ); 69 | setSyncNoteVisibility(!gsStorage.getOption(gsStorage.SYNC_SETTINGS)); 70 | 71 | let searchParams = new URL(location.href).searchParams; 72 | if (searchParams.has('firstTime')) { 73 | document 74 | .querySelector('.welcome-message') 75 | .classList.remove('reallyHidden'); 76 | document.querySelector('#options-heading').classList.add('reallyHidden'); 77 | } 78 | 79 | var discardSet = gsStorage.getOption(gsStorage.DISCARD_IN_PLACE_OF_SUSPEND); 80 | showHideSuspendOnlyOptions(discardSet); 81 | } 82 | 83 | function populateOption(element, value) { 84 | if ( 85 | element.tagName === 'INPUT' && 86 | element.hasAttribute('type') && 87 | element.getAttribute('type') === 'checkbox' 88 | ) { 89 | element.checked = value; 90 | } else if (element.tagName === 'SELECT') { 91 | selectComboBox(element, value); 92 | } else if (element.tagName === 'TEXTAREA') { 93 | element.value = value; 94 | } 95 | } 96 | 97 | function getOptionValue(element) { 98 | if ( 99 | element.tagName === 'INPUT' && 100 | element.hasAttribute('type') && 101 | element.getAttribute('type') === 'checkbox' 102 | ) { 103 | return element.checked; 104 | } 105 | if (element.tagName === 'SELECT') { 106 | return element.children[element.selectedIndex].value; 107 | } 108 | if (element.tagName === 'TEXTAREA') { 109 | return element.value; 110 | } 111 | } 112 | 113 | function setForceScreenCaptureVisibility(visible) { 114 | if (visible) { 115 | document.getElementById('forceScreenCaptureContainer').style.display = 116 | 'block'; 117 | } else { 118 | document.getElementById('forceScreenCaptureContainer').style.display = 119 | 'none'; 120 | } 121 | } 122 | 123 | function setSyncNoteVisibility(visible) { 124 | if (visible) { 125 | document.getElementById('syncNote').style.display = 'block'; 126 | } else { 127 | document.getElementById('syncNote').style.display = 'none'; 128 | } 129 | } 130 | 131 | function setAutoSuspendOptionsVisibility(visible) { 132 | Array.prototype.forEach.call( 133 | document.getElementsByClassName('autoSuspendOption'), 134 | function(el) { 135 | if (visible) { 136 | el.style.display = 'block'; 137 | } else { 138 | el.style.display = 'none'; 139 | } 140 | } 141 | ); 142 | } 143 | 144 | function showHideSuspendOnlyOptions(hide) { 145 | hideWhenDiscardSet.forEach(elementId => { 146 | console.log(elementId); 147 | element = document.getElementById(elementId); 148 | console.log(element); 149 | element.style.visibility = hide ? 'hidden' : 'visible'; 150 | }); 151 | } 152 | 153 | function handleChange(element) { 154 | return function() { 155 | console.log(element); 156 | var pref = elementPrefMap[element.id], 157 | interval; 158 | 159 | //add specific screen element listeners 160 | if (pref === gsStorage.SCREEN_CAPTURE) { 161 | setForceScreenCaptureVisibility(getOptionValue(element) !== '0'); 162 | } else if (pref === gsStorage.SUSPEND_TIME) { 163 | interval = getOptionValue(element); 164 | setAutoSuspendOptionsVisibility(interval > 0); 165 | } else if (pref === gsStorage.SYNC_SETTINGS) { 166 | // we only really want to show this on load. not on toggle 167 | if (getOptionValue(element)) { 168 | setSyncNoteVisibility(false); 169 | } 170 | } else if (pref == gsStorage.DISCARD_IN_PLACE_OF_SUSPEND) { 171 | var discardSet = getOptionValue(element); 172 | showHideSuspendOnlyOptions(discardSet); 173 | } 174 | 175 | var [oldValue, newValue] = saveChange(element); 176 | if (oldValue !== newValue) { 177 | var prefKey = elementPrefMap[element.id]; 178 | gsUtils.performPostSaveUpdates( 179 | [prefKey], 180 | { [prefKey]: oldValue }, 181 | { [prefKey]: newValue } 182 | ); 183 | } 184 | }; 185 | } 186 | 187 | function saveChange(element) { 188 | var pref = elementPrefMap[element.id], 189 | oldValue = gsStorage.getOption(pref), 190 | newValue = getOptionValue(element); 191 | 192 | //clean up whitelist before saving 193 | if (pref === gsStorage.WHITELIST) { 194 | newValue = gsUtils.cleanupWhitelist(newValue); 195 | } 196 | 197 | //save option 198 | if (oldValue !== newValue) { 199 | gsStorage.setOptionAndSync(elementPrefMap[element.id], newValue); 200 | } 201 | 202 | return [oldValue, newValue]; 203 | } 204 | 205 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 206 | initSettings(); 207 | 208 | var optionEls = document.getElementsByClassName('option'), 209 | element, 210 | i; 211 | 212 | //add change listeners for all 'option' elements 213 | for (i = 0; i < optionEls.length; i++) { 214 | element = optionEls[i]; 215 | if (element.tagName === 'TEXTAREA') { 216 | element.addEventListener( 217 | 'input', 218 | gsUtils.debounce(handleChange(element), 200), 219 | false 220 | ); 221 | } else { 222 | element.onchange = handleChange(element); 223 | } 224 | } 225 | 226 | document.getElementById('testWhitelistBtn').onclick = async e => { 227 | e.preventDefault(); 228 | const tabs = await gsChrome.tabsQuery(); 229 | const tabUrls = tabs 230 | .map( 231 | tab => 232 | gsUtils.isSuspendedTab(tab) 233 | ? gsUtils.getOriginalUrl(tab.url) 234 | : tab.url 235 | ) 236 | .filter( 237 | url => !gsUtils.isSuspendedUrl(url) && gsUtils.checkWhiteList(url) 238 | ) 239 | .map(url => (url.length > 55 ? url.substr(0, 52) + '...' : url)); 240 | if (tabUrls.length === 0) { 241 | alert(chrome.i18n.getMessage('js_options_whitelist_no_matches')); 242 | return; 243 | } 244 | const firstUrls = tabUrls.splice(0, 22); 245 | let alertString = `${chrome.i18n.getMessage( 246 | 'js_options_whitelist_matches_heading' 247 | )}\n${firstUrls.join('\n')}`; 248 | 249 | if (tabUrls.length > 0) { 250 | alertString += `\n${chrome.i18n.getMessage( 251 | 'js_options_whitelist_matches_overflow_prefix' 252 | )} ${tabUrls.length} ${chrome.i18n.getMessage( 253 | 'js_options_whitelist_matches_overflow_suffix' 254 | )}`; 255 | } 256 | alert(alertString); 257 | }; 258 | 259 | //hide incompatible sidebar items if in incognito mode 260 | if (chrome.extension.inIncognitoContext) { 261 | Array.prototype.forEach.call( 262 | document.getElementsByClassName('noIncognito'), 263 | function(el) { 264 | el.style.display = 'none'; 265 | } 266 | ); 267 | window.alert(chrome.i18n.getMessage('js_options_incognito_warning')); 268 | } 269 | }); 270 | 271 | global.exports = { 272 | initSettings, 273 | }; 274 | })(this); 275 | -------------------------------------------------------------------------------- /src/js/permissions.js: -------------------------------------------------------------------------------- 1 | (function(global) { 2 | 'use strict'; 3 | 4 | try { 5 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 6 | } catch (e) { 7 | window.setTimeout(() => window.location.reload(), 1000); 8 | return; 9 | } 10 | 11 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 12 | document.getElementById('exportBackupBtn').onclick = async function(e) { 13 | const currentSession = await gsSession.buildCurrentSession(); 14 | historyUtils.exportSession(currentSession, function() { 15 | document.getElementById('exportBackupBtn').style.display = 'none'; 16 | }); 17 | }; 18 | document.getElementById('setFilePermissiosnBtn').onclick = async function( 19 | e 20 | ) { 21 | await gsChrome.tabsCreate({ 22 | url: 'about:addons?id=' + browser.extension.getURL ('.').replace('moz-extension:', '').replace('/', ''), 23 | }); 24 | }; 25 | }); 26 | })(this); 27 | -------------------------------------------------------------------------------- /src/js/recovery.js: -------------------------------------------------------------------------------- 1 | /*global chrome, historyItems, gsMessages, gsSession, gsStorage, gsIndexedDb, gsChrome, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | var restoreAttempted = false; 13 | var tabsToRecover = []; 14 | 15 | async function getRecoverableTabs(currentTabs) { 16 | const lastSession = await gsIndexedDb.fetchLastSession(); 17 | //check to see if they still exist in current session 18 | if (lastSession) { 19 | gsUtils.removeInternalUrlsFromSession(lastSession); 20 | for (const window of lastSession.windows) { 21 | for (const tabProperties of window.tabs) { 22 | if (gsUtils.isSuspendedTab(tabProperties)) { 23 | var originalUrl = gsUtils.getOriginalUrl(tabProperties.url); 24 | // Ignore suspended tabs from previous session that exist unsuspended now 25 | const originalTab = currentTabs.find(o => o.url === originalUrl); 26 | if (!originalTab) { 27 | tabProperties.windowId = window.id; 28 | tabProperties.sessionId = lastSession.sessionId; 29 | tabsToRecover.push(tabProperties); 30 | } 31 | } 32 | } 33 | } 34 | return tabsToRecover; 35 | } 36 | } 37 | 38 | function removeTabFromList(tabToRemove) { 39 | const recoveryTabsEl = document.getElementById('recoveryTabs'); 40 | const childLinks = recoveryTabsEl.children; 41 | 42 | for (var i = 0; i < childLinks.length; i++) { 43 | const element = childLinks[i]; 44 | const url = gsUtils.isSuspendedTab(tabToRemove) 45 | ? gsUtils.getOriginalUrl(tabToRemove.url) 46 | : tabToRemove.url; 47 | 48 | if ( 49 | element.getAttribute('data-url') === url || 50 | element.getAttribute('data-tabId') == tabToRemove.id 51 | ) { 52 | // eslint-disable-line eqeqeq 53 | recoveryTabsEl.removeChild(element); 54 | } 55 | } 56 | 57 | //if removing the last element.. (re-get the element this function gets called asynchronously 58 | if (document.getElementById('recoveryTabs').children.length === 0) { 59 | //if we have already clicked the restore button then redirect to success page 60 | if (restoreAttempted) { 61 | document.getElementById('suspendy-guy-inprogress').style.display = 62 | 'none'; 63 | document.getElementById('recovery-inprogress').style.display = 'none'; 64 | document.getElementById('suspendy-guy-complete').style.display = 65 | 'inline-block'; 66 | document.getElementById('recovery-complete').style.display = 67 | 'inline-block'; 68 | 69 | //otherwise we have no tabs to recover so just hide references to recovery 70 | } else { 71 | hideRecoverySection(); 72 | } 73 | } 74 | } 75 | 76 | function showTabSpinners() { 77 | var recoveryTabsEl = document.getElementById('recoveryTabs'), 78 | childLinks = recoveryTabsEl.children; 79 | 80 | for (var i = 0; i < childLinks.length; i++) { 81 | var tabContainerEl = childLinks[i]; 82 | tabContainerEl.removeChild(tabContainerEl.firstChild); 83 | var spinnerEl = document.createElement('span'); 84 | spinnerEl.classList.add('faviconSpinner'); 85 | tabContainerEl.insertBefore(spinnerEl, tabContainerEl.firstChild); 86 | } 87 | } 88 | 89 | function hideRecoverySection() { 90 | var recoverySectionEls = document.getElementsByClassName('recoverySection'); 91 | for (var i = 0; i < recoverySectionEls.length; i++) { 92 | recoverySectionEls[i].style.display = 'none'; 93 | } 94 | document.getElementById('restoreSession').style.display = 'none'; 95 | } 96 | 97 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(async function() { 98 | var restoreEl = document.getElementById('restoreSession'), 99 | manageEl = document.getElementById('manageManuallyLink'), 100 | previewsEl = document.getElementById('previewsOffBtn'), 101 | recoveryEl = document.getElementById('recoveryTabs'), 102 | warningEl = document.getElementById('screenCaptureNotice'), 103 | tabEl; 104 | 105 | manageEl.onclick = function(e) { 106 | e.preventDefault(); 107 | chrome.tabs.create({ url: chrome.extension.getURL('history.html') }); 108 | }; 109 | 110 | if (previewsEl) { 111 | previewsEl.onclick = function(e) { 112 | gsStorage.setOptionAndSync(gsStorage.SCREEN_CAPTURE, '0'); 113 | window.location.reload(); 114 | }; 115 | 116 | //show warning if screen capturing turned on 117 | if (gsStorage.getOption(gsStorage.SCREEN_CAPTURE) !== '0') { 118 | warningEl.style.display = 'block'; 119 | } 120 | } 121 | 122 | var performRestore = async function() { 123 | restoreAttempted = true; 124 | restoreEl.className += ' btnDisabled'; 125 | restoreEl.removeEventListener('click', performRestore); 126 | showTabSpinners(); 127 | while (gsSession.isInitialising()) { 128 | await gsUtils.setTimeout(200); 129 | } 130 | await gsSession.recoverLostTabs(); 131 | }; 132 | 133 | restoreEl.addEventListener('click', performRestore); 134 | 135 | const currentTabs = await gsChrome.tabsQuery(); 136 | const tabsToRecover = await getRecoverableTabs(currentTabs); 137 | if (tabsToRecover.length === 0) { 138 | hideRecoverySection(); 139 | return; 140 | } 141 | 142 | for (var tabToRecover of tabsToRecover) { 143 | tabToRecover.title = gsUtils.getCleanTabTitle(tabToRecover); 144 | tabToRecover.url = gsUtils.getOriginalUrl(tabToRecover.url); 145 | tabEl = await historyItems.createTabHtml(tabToRecover, false); 146 | tabEl.onclick = function() { 147 | return function(e) { 148 | e.preventDefault(); 149 | chrome.tabs.create({ url: tabToRecover.url, active: false }); 150 | removeTabFromList(tabToRecover); 151 | }; 152 | }; 153 | recoveryEl.appendChild(tabEl); 154 | } 155 | 156 | var currentSuspendedTabs = currentTabs.filter(o => 157 | gsUtils.isSuspendedTab(o) 158 | ); 159 | for (const suspendedTab of currentSuspendedTabs) { 160 | gsMessages.sendPingToTab(suspendedTab.id, function(error) { 161 | if (error) { 162 | gsUtils.warning(suspendedTab.id, 'Failed to sendPingToTab', error); 163 | } else { 164 | removeTabFromList(suspendedTab); 165 | } 166 | }); 167 | } 168 | }); 169 | 170 | global.exports = { 171 | removeTabFromList, 172 | }; 173 | })(this); 174 | -------------------------------------------------------------------------------- /src/js/restoring-window.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 13 | //do nothing 14 | }); 15 | })(this); 16 | -------------------------------------------------------------------------------- /src/js/shortcuts.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 13 | var shortcutsEl = document.getElementById('keyboardShortcuts'); 14 | var configureShortcutsEl = document.getElementById('configureShortcuts'); 15 | 16 | var notSetMessage = chrome.i18n.getMessage('js_shortcuts_not_set'); 17 | var groupingKeys = [ 18 | '2-toggle-temp-whitelist-tab', 19 | '2b-unsuspend-selected-tabs', 20 | '4-unsuspend-active-window', 21 | ]; 22 | 23 | //populate keyboard shortcuts 24 | chrome.commands.getAll(commands => { 25 | commands.forEach(command => { 26 | if (command.name !== '_execute_browser_action') { 27 | const shortcut = 28 | (command.shortcut !== '' && command.shortcut !== null) 29 | ? gsUtils.formatHotkeyString(command.shortcut) 30 | : '(' + notSetMessage + ')'; 31 | var addMarginBottom = groupingKeys.includes(command.name); 32 | shortcutsEl.innerHTML += `
${command.description}
35 |
${shortcut}
`; 38 | } 39 | }); 40 | }); 41 | 42 | //listener for configureShortcuts 43 | configureShortcutsEl.onclick = function(e) { 44 | chrome.tabs.update({ url: 'chrome://extensions/shortcuts' }); 45 | }; 46 | }); 47 | 48 | })(this); 49 | -------------------------------------------------------------------------------- /src/js/tests/fixture_currentSessions.json: -------------------------------------------------------------------------------- 1 | { 2 | "currentSession1": { 3 | "sessionId": "111111", 4 | "windows": [ 5 | { 6 | "alwaysOnTop": false, 7 | "focused": true, 8 | "height": 1027, 9 | "id": 3628, 10 | "incognito": false, 11 | "left": 0, 12 | "state": "maximized", 13 | "tabs": [ 14 | { 15 | "active": true, 16 | "audible": false, 17 | "autoDiscardable": true, 18 | "discarded": false, 19 | "favIconUrl": "https://ssl.gstatic.com/ui/v1/icons/mail/images/favicon5.ico", 20 | "height": 953, 21 | "highlighted": true, 22 | "id": 3630, 23 | "incognito": false, 24 | "index": 0, 25 | "mutedInfo": { 26 | "muted": false 27 | }, 28 | "pinned": false, 29 | "selected": true, 30 | "status": "loading", 31 | "title": "Gmail", 32 | "url": "https://mail.google.com/mail/u/0/#inbox", 33 | "width": 1680, 34 | "windowId": 3628 35 | }, 36 | { 37 | "active": false, 38 | "audible": false, 39 | "autoDiscardable": true, 40 | "discarded": false, 41 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEnklEQVRYR+2XT2xUVRTGf+9yO47jY5zUOpmURiwYMYQgmki0ykI2GAUUbYxRQkB3xIVKUDZ00i5MjOHfXo00xKAQiEKaqCRoAE20ERaVVAxUQksmFSdjeT7H6e195r4/05lpi9MyiS68SZO2771zvvvd75z7HYtorXntQe5sX8Vtd9zO/LSHbBLlZ434RY1rro9a/PHb7/w6dIrP9/5gwlr+z4au9Sx5fAWxOCgFWjUi5dQYQoKUUCrCTyfPcbTnMwuz8wc2rEM2acZdC2ueAYUQAmE+8JcuB9NKVfxVncNQJkyC8gpI1FqhdRjDm/BoSnioccHZo8csNu55g/ZHbEoOUXJMYseFolOZO0BvJyEmTNSa7AJKGpyxgMVoGQxxG+zEJLMGRMyGoW8di60fZ0llwoAmqCThXuWZhZK7mxNIE8AkE1DUkq8uFvjOSUHCBAxBCAGuy8pEnsfak9gxn8MQguCXQpHDF4u4iTagFDwzmyxcxeLNEzsR8wSW52E1WUlnmC+3LGPliqUVQSY3pJwCW947wYHLSUTCZBJot8jmxS7vv7wakUhOq5/T/ed4av8gY8k2mBj3sCwLPaEtdnydBeWftx5z2bSkxP5X16N0cHaivBPzliYuJQODF7h/Xz+6+S5fHyI/zI/bHua+exZRUtXfaPNcSJ/Jl/Ye4aMhG2HHQ00ILHaczBpViZhE50bpWpUgu/EJlFbEqgRlAJlggnw+z8K3DuO0GJbAzg9y5Z1OUqlUeFzVFWxASSHZ2dvH22eKiEwaXVL+SQQAzMnLGCp3la5H43RvXuvvpBZAxO1YocCCbQdxMssCALkBRnY9TzLV7FdIbQOJYmV7j9NzqojMtKKU0UIFAFM+AQNxujc9iVIa6StwckUMjBXyLNj+CU46BDB6npF3OwMAIUuV30Wxsh/20RMxEFZKBQMS9Q8AqhjYfrAGgGEgNa0AywB6+0IG0qj/ATSMAQ32NaOBOo4g1IDMNPoIcucZ2VWPCI/Tc6YUlGFDGRg1ZfjCjCKcLEMjQheZyTRIhC2mDDXxwiUud6+luaXZ7wGmWUXLlKXS2u8p2z7oY/f3CtGSChtR1An9RjS3MhRodOEah57L0Lm6Y9oy9P9Zcnkoe4R+1Wbuu/K6yT6wHKGLaC1p1aPsW9NKx7JF2PHgkjLsaASXhnPs+WKAA1fs6lu0shPOlQFMSzWUawFOnoQuIFSxwg9IXGGj482QiIeeYPKIbpoBtEkWBiw7qMoWHngG/6hqTUxD7gIdGozqW6NGCzP72xoGcrO8DZdTxcDMEpzxSegHNCIWm70fSC+FaRmoH0mZAVO7lY7I+EtjzysdkdG0qecqRxSJsP6cVW+Gliwyl5JJTxjc9bWr5BR4pewJI2s1x+yYRhSZUs/ysDzL6NV2czy7OEZbMoYUxhwGQneNK/75Gv1uy5R6nhUEy/LwvNCUbj2UJZWu9vk3mguSSXyHOU1J1Q3Cd/8CCqNYvLj7de7tmM+f0WASdLBgMppaPjeajOoGYAaTW2248M31cDR7eh3ylqrRrO5gs32xPJr9JTj76bH/wHAa7eBfGs//Bo/rCZjlAhJQAAAAAElFTkSuQmCC", 42 | "height": 914, 43 | "highlighted": false, 44 | "id": 3631, 45 | "incognito": false, 46 | "index": 1, 47 | "mutedInfo": { 48 | "muted": false 49 | }, 50 | "pinned": false, 51 | "selected": false, 52 | "status": "complete", 53 | "title": "Trello", 54 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Ftrello.com&pos=0&uri=https://trello.com", 55 | "width": 1680, 56 | "windowId": 3628 57 | }, 58 | { 59 | "active": false, 60 | "audible": false, 61 | "autoDiscardable": true, 62 | "discarded": false, 63 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACR0lEQVQ4T42Tv2sUQRTH35udvdwl8W72Zk6DRBILtdGAprS2MkgUFKwURWwUBQUL/wI7C1FBFK3URhRsRNDCxkIt1EJBMMFgkczu7cWQvbudnSez7OkS5HCreb8+b9737SBs+IQQU5zz3US0HQA2FeFfiPjdGPM5juPFcgmWDJRCzHucz2yElu3MmI9hHD8FAHL+AQCbzeYpztjksOJBzFi7FEXRPQfJAVKIIx7nezJjFtd7vZdJkuyo1+szvu9bF2cAjZUwfF2r1RZHR0YOeJxPZcZ8CuP4ies8yRk77RLTLHvfbrefA0CluGJadHV2352DIJjzPW/WnY21d93chwdzW4A7Wuufw8YQQkxXOD/hcpwe2Gq1LiCRIKIszbIbcRzHwwBBEDQ4Y+cR0SPEGFtSXkVEbgE6Wuvr/yOiUuqi04WIzB9ARtQLw/DaYD1DQCilvOIhjuQAKeU5D1G6AgvwSGv9ddgtlFI7GcDxXAOiCJVShwBgr9aaRL1OLE3vR0ny41+QZq22jY2NHWMA48XWPjgRJ3pJ7+zq2mrmVsUYq7kNCSEeRFG05BKr1ep0v98/qprN0TJ4vdu9lf9ISqk5BjDrHL7vTxDRPiJ61ul02kV8qzHmTIXzv/WMvV1eXn7x5y1IIU6C522x1t5uNBq9hYWFbqlbpSXlZUT0nY+s/bISRY/LbyHPVUodBGtnCbEbhuFNAFgrIOOblbqUC5emb8JO59UAXn6NuS8Igv2IuCuKoocAkBSJVRUE8+B537TW78o6/AYZcu5hODEeGgAAAABJRU5ErkJggg==", 64 | "height": 914, 65 | "highlighted": false, 66 | "id": 3632, 67 | "incognito": false, 68 | "index": 2, 69 | "mutedInfo": { 70 | "muted": false 71 | }, 72 | "pinned": false, 73 | "selected": false, 74 | "status": "complete", 75 | "title": "https://github.com/deanoemcke/thegreatsuspender/issues/266", 76 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Fgithub.com%2Fdeanoemcke%2Fthegreatsuspender%2Fissues%2F266&pos=0&uri=https://github.com/deanoemcke/thegreatsuspender/issues/266", 77 | "width": 1680, 78 | "windowId": 3628 79 | }, 80 | { 81 | "active": false, 82 | "audible": false, 83 | "autoDiscardable": true, 84 | "discarded": false, 85 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGB0lEQVRYR8WXbWgUVxSG37l7M7k7DUsMEtM0aKLpIiGVyqqxXZWtWWtitFJKFG3BftCm9oMQioT+kLaEImLbkIoULUGltFJsmx9+1EJq/dGIFYIlSFqCpGmIaZqWNQ3rZnYyuVPOzM5mN7ubCC04hOzs3Jn7Puc95565q+A+H8p91sc9A1jPh8SdaV+A+bylYJ7liiWFCy9NCWtG6pZhDCrx6dFF2lSvcuqKfi/BLQjwWygkvIs8YY8nL8BUDiZUZ17OZ+c3TUjDBKSE1A1IKWEZZu9UDN0VV+YHmRfgj9qaKo/w7rAsCJA45yAI+2BsFoCEUwBoQMYMSFPqbDp+7sGfbvTnciMnwO21a8PIV4IkautpicgJJFWcxKQECCBxOOLOdzpnltLz0I0b3dkgsgLcXr06LC0ryFQGitwGmAuSMpstoqmJqBMghkkOAPQXM2DC6lkx8EsGRAbA70srqyDUxmTEjMQTdrv2pwBRpARnR5xwwRV2HbA/6T5Tnl02fCstHWkAoR9C4pEfjebXOseFpjqWJ0HsLykwc/zMJuoKz96q6JDxjoqhoeQKSQPYcWHbdgsy8GzHMB4dMEAQboTuJK7VKCsBioudy8YYEBubg2RkrTuPF71lXYPn3cEkAEVfEBOtNOAbieHNIyMoNBlESsHZdfD4erD9TYgULHaKjAtozISGccjhDnB9Ns1clVBVpyYYo3MGqerQCmOHlQrYLiQB6i/WBz2WFXbJNp4ewZNXoxCcJyFYXR0iza24PKjjz6gET9SGaUosKWAI+zWUTByEGv0mKci4Ca6mu8EUpVupnupJA3jq24ZGKWeqXAB1XMdL7w+iVGc2hBbahMjbbfj02gR0Q0IIDjUBYNjVLu1HmzYUodLYD2FeypoCumhZSj9fNXU2DaDhfH2roljJ9kqD/i9HsPNSxAYo/+4C2m8yDEcMW9jtBUJlNhD1AgJZXizwVsiAb6wmJ4AZz9fz1/xzOA1gW1fdOyzRa9wnZdTAzoO/orrEj8LPvkbLFwPQBAdnLBm9ey+JmwmII7sqURnbCm7ezAphGiryA5PvLQhANxR8P4b9/aWYPNSJ9otDINcp9+QAnZP7djFKCaoF+n5k70pUm8+A61f/OwDNsLVLQ/nrx9DWdQtqntOU1MTqSIUwyIFpifY9fqzhe3ICGDEN3prIvTlAYmXRErRsOordR6874nZbTnkZOR7Y/w3TxMl9K7GKPQ3ofRkOkP3S5JkA9We2tipCEXPrwJ2hc/Np7P6oD2N3DPAEANUCHZR7AjBNE5VLNJx5sRjaaCCn/XE9X/cF/0ovwi2f1zXmaajKBfBYaRB1hW9g39HLiBqZLZlacYEKHH95A9bwJnA9cxm60cenvP2Fodvpy3DLqS1BT74SZoIhF0TzqhZok360dF7H6EQsLcLSQg3HmzZAM0+hUrZljZ5yb5ocVpx3+zaPpjei0MkQtZZWLgDum5vf2fnWFq/D3rJ9GI8wXBsYtwfW+4tRXCTx1c/H8ErxCRQm2m8qhSPOQC5MQD1c8YTzQkp7GdWeqN2ueD0BSvF8EPRgua8cApqtMRmNYnBsCAfKbiJcEsmInsTp0GMC8ZjoLWnI8jKiG8gFNp3XzLyKUAscF3KlI1XFnJTwswgO+fsheKIxUHEaTmczdNU+1++quv4AOtzoMxygCxs/rq3K8yqNVAvkxHw14UJQx/ywrA9+XzTJ5RYc5dw0OPSYCuMuP7ts1zwbEvfp9e3hsFaAIO2EqCYIIpcb0gAaxDBeLRtMRkzr3M23naK/NZiS9azYPbDwliwVgnMZVGlVpLjhjlNqSNwndXyyuA9CwBalBkUROzlXYcR4TvGsKUjNbc0HtVUexdrBBQRXmbM3pAJN7MxpG3igaAjrtFnrSdTOOwkbHl1aM+cefi7d9lSNBX+YlL8bEkUehFWNBbjq/BwgGDqqeQytRaMpeXetB4wZ1lug6N0VL8zu/zKWx9xlmO0G9xqBLFZnApZkpflez3IJiDbfOJaq9BuA0SZDZ1AGgZnRfE+8dyFhd94FHZgP6v8Yu+8A/wKjtIs/vGW6ZwAAAABJRU5ErkJggg==", 86 | "height": 914, 87 | "highlighted": false, 88 | "id": 3633, 89 | "incognito": false, 90 | "index": 3, 91 | "mutedInfo": { 92 | "muted": false 93 | }, 94 | "pinned": false, 95 | "selected": false, 96 | "status": "complete", 97 | "title": "chrome.i18n - Google Chrome", 98 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Fdeveloper.chrome.com%2Fextensions%2Fi18n&pos=0&uri=https://developer.chrome.com/extensions/i18n", 99 | "width": 1680, 100 | "windowId": 3628 101 | }, 102 | { 103 | "active": false, 104 | "audible": false, 105 | "autoDiscardable": true, 106 | "discarded": false, 107 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB4klEQVRYR+1XPUgcQRT+3o6rHkg8JChyIVUKo0Uqr4hFFASx0O5AEEJMfcKdB6mUrIiNoCd4jSB2doKChaU/WF2dgFU6IQlKjCieuDMvzK6nrtzdHriwKXaq95jvvffNt28es4SQF4VcHxGBSAHiVErs73wbMIjfKUZL3U2ZGT9wsMsbH6rFEITiRAeLoSShNU4sJSmWEoTzJtPYej8/fUJ7zW8HAe6ru3AZmB53rcKGb2hj9xvEsxMOzi6VcPnzN+y/lzAlrdFBrCvnnHy0H3id8E12X/ApgbL/OEPpBljbBIwGtK/OenJrEle/zv7QXnPXV2ennEApQEnAtgGlN7R/F2sAWN92nWcSUErh7PuxeiDgf3YvwucTaK5l3lV75FUn01FLd8+tVCNgNLlBJAkkQSwZpAyAGYoBwQwWxMoEhMmZscNaTahzMQRrDEN6rjvDZKOzzebB3h/RHKCPnz67TRjSCp9AOpcbhuSkRwBBxcLi4m4lUYLGUza7FJN8MqnAMV3QAF0LSqzk81PXlQgEjXduwWT2S5LZHtY2UcPuSn6hWKslgsQ7BCzLMk7PL2a0/TL+Ys6yrJozJEj8/RxIZ6ac21BYXvIO7SpSBIWPCEQKRApECvw/CoT0HAj/5/QffopgMc7Z6fsAAAAASUVORK5CYII=", 108 | "height": 914, 109 | "highlighted": false, 110 | "id": 3634, 111 | "incognito": false, 112 | "index": 4, 113 | "mutedInfo": { 114 | "muted": false 115 | }, 116 | "pinned": false, 117 | "selected": false, 118 | "status": "complete", 119 | "title": "447856 - Feature Request: Ability for an extension to change the URL in the omnibox - chromium - Monorail", 120 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Fbugs.chromium.org%2Fp%2Fchromium%2Fissues%2Fdetail%3Fid%3D447856&pos=0&uri=https://bugs.chromium.org/p/chromium/issues/detail?id=447856", 121 | "width": 1680, 122 | "windowId": 3628 123 | } 124 | ], 125 | "top": 23, 126 | "type": "normal", 127 | "width": 1680 128 | } 129 | ], 130 | "date": "2018-08-29T14:07:04.041Z", 131 | "name": "currentSession1", 132 | "id": 1 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/js/tests/fixture_savedSessions.json: -------------------------------------------------------------------------------- 1 | { 2 | "savedSession1": { 3 | "sessionId": "_111111", 4 | "windows": [ 5 | { 6 | "alwaysOnTop": false, 7 | "focused": true, 8 | "height": 1027, 9 | "id": 3628, 10 | "incognito": false, 11 | "left": 0, 12 | "state": "maximized", 13 | "tabs": [ 14 | { 15 | "active": true, 16 | "audible": false, 17 | "autoDiscardable": true, 18 | "discarded": false, 19 | "favIconUrl": "https://ssl.gstatic.com/ui/v1/icons/mail/images/favicon5.ico", 20 | "height": 953, 21 | "highlighted": true, 22 | "id": 3630, 23 | "incognito": false, 24 | "index": 0, 25 | "mutedInfo": { 26 | "muted": false 27 | }, 28 | "pinned": false, 29 | "selected": true, 30 | "status": "loading", 31 | "title": "Gmail", 32 | "url": "https://mail.google.com/mail/u/0/#inbox", 33 | "width": 1680, 34 | "windowId": 3628 35 | }, 36 | { 37 | "active": false, 38 | "audible": false, 39 | "autoDiscardable": true, 40 | "discarded": false, 41 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAEnklEQVRYR+2XT2xUVRTGf+9yO47jY5zUOpmURiwYMYQgmki0ykI2GAUUbYxRQkB3xIVKUDZ00i5MjOHfXo00xKAQiEKaqCRoAE20ERaVVAxUQksmFSdjeT7H6e195r4/05lpi9MyiS68SZO2771zvvvd75z7HYtorXntQe5sX8Vtd9zO/LSHbBLlZ434RY1rro9a/PHb7/w6dIrP9/5gwlr+z4au9Sx5fAWxOCgFWjUi5dQYQoKUUCrCTyfPcbTnMwuz8wc2rEM2acZdC2ueAYUQAmE+8JcuB9NKVfxVncNQJkyC8gpI1FqhdRjDm/BoSnioccHZo8csNu55g/ZHbEoOUXJMYseFolOZO0BvJyEmTNSa7AJKGpyxgMVoGQxxG+zEJLMGRMyGoW8di60fZ0llwoAmqCThXuWZhZK7mxNIE8AkE1DUkq8uFvjOSUHCBAxBCAGuy8pEnsfak9gxn8MQguCXQpHDF4u4iTagFDwzmyxcxeLNEzsR8wSW52E1WUlnmC+3LGPliqUVQSY3pJwCW947wYHLSUTCZBJot8jmxS7vv7wakUhOq5/T/ed4av8gY8k2mBj3sCwLPaEtdnydBeWftx5z2bSkxP5X16N0cHaivBPzliYuJQODF7h/Xz+6+S5fHyI/zI/bHua+exZRUtXfaPNcSJ/Jl/Ye4aMhG2HHQ00ILHaczBpViZhE50bpWpUgu/EJlFbEqgRlAJlggnw+z8K3DuO0GJbAzg9y5Z1OUqlUeFzVFWxASSHZ2dvH22eKiEwaXVL+SQQAzMnLGCp3la5H43RvXuvvpBZAxO1YocCCbQdxMssCALkBRnY9TzLV7FdIbQOJYmV7j9NzqojMtKKU0UIFAFM+AQNxujc9iVIa6StwckUMjBXyLNj+CU46BDB6npF3OwMAIUuV30Wxsh/20RMxEFZKBQMS9Q8AqhjYfrAGgGEgNa0AywB6+0IG0qj/ATSMAQ32NaOBOo4g1IDMNPoIcucZ2VWPCI/Tc6YUlGFDGRg1ZfjCjCKcLEMjQheZyTRIhC2mDDXxwiUud6+luaXZ7wGmWUXLlKXS2u8p2z7oY/f3CtGSChtR1An9RjS3MhRodOEah57L0Lm6Y9oy9P9Zcnkoe4R+1Wbuu/K6yT6wHKGLaC1p1aPsW9NKx7JF2PHgkjLsaASXhnPs+WKAA1fs6lu0shPOlQFMSzWUawFOnoQuIFSxwg9IXGGj482QiIeeYPKIbpoBtEkWBiw7qMoWHngG/6hqTUxD7gIdGozqW6NGCzP72xoGcrO8DZdTxcDMEpzxSegHNCIWm70fSC+FaRmoH0mZAVO7lY7I+EtjzysdkdG0qecqRxSJsP6cVW+Gliwyl5JJTxjc9bWr5BR4pewJI2s1x+yYRhSZUs/ysDzL6NV2czy7OEZbMoYUxhwGQneNK/75Gv1uy5R6nhUEy/LwvNCUbj2UJZWu9vk3mguSSXyHOU1J1Q3Cd/8CCqNYvLj7de7tmM+f0WASdLBgMppaPjeajOoGYAaTW2248M31cDR7eh3ylqrRrO5gs32xPJr9JTj76bH/wHAa7eBfGs//Bo/rCZjlAhJQAAAAAElFTkSuQmCC", 42 | "height": 914, 43 | "highlighted": false, 44 | "id": 3631, 45 | "incognito": false, 46 | "index": 1, 47 | "mutedInfo": { 48 | "muted": false 49 | }, 50 | "pinned": false, 51 | "selected": false, 52 | "status": "complete", 53 | "title": "the great suspender | Trello", 54 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Ftrello.com%2Fb%2FCNT09XJg%2Fthe-great-suspender&pos=0&uri=https://trello.com/b/CNT09XJg/the-great-suspender", 55 | "width": 1680, 56 | "windowId": 3628 57 | }, 58 | { 59 | "active": false, 60 | "audible": false, 61 | "autoDiscardable": true, 62 | "discarded": false, 63 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACR0lEQVQ4T42Tv2sUQRTH35udvdwl8W72Zk6DRBILtdGAprS2MkgUFKwURWwUBQUL/wI7C1FBFK3URhRsRNDCxkIt1EJBMMFgkczu7cWQvbudnSez7OkS5HCreb8+b9737SBs+IQQU5zz3US0HQA2FeFfiPjdGPM5juPFcgmWDJRCzHucz2yElu3MmI9hHD8FAHL+AQCbzeYpztjksOJBzFi7FEXRPQfJAVKIIx7nezJjFtd7vZdJkuyo1+szvu9bF2cAjZUwfF2r1RZHR0YOeJxPZcZ8CuP4ies8yRk77RLTLHvfbrefA0CluGJadHV2352DIJjzPW/WnY21d93chwdzW4A7Wuufw8YQQkxXOD/hcpwe2Gq1LiCRIKIszbIbcRzHwwBBEDQ4Y+cR0SPEGFtSXkVEbgE6Wuvr/yOiUuqi04WIzB9ARtQLw/DaYD1DQCilvOIhjuQAKeU5D1G6AgvwSGv9ddgtlFI7GcDxXAOiCJVShwBgr9aaRL1OLE3vR0ny41+QZq22jY2NHWMA48XWPjgRJ3pJ7+zq2mrmVsUYq7kNCSEeRFG05BKr1ep0v98/qprN0TJ4vdu9lf9ISqk5BjDrHL7vTxDRPiJ61ul02kV8qzHmTIXzv/WMvV1eXn7x5y1IIU6C522x1t5uNBq9hYWFbqlbpSXlZUT0nY+s/bISRY/LbyHPVUodBGtnCbEbhuFNAFgrIOOblbqUC5emb8JO59UAXn6NuS8Igv2IuCuKoocAkBSJVRUE8+B537TW78o6/AYZcu5hODEeGgAAAABJRU5ErkJggg==", 64 | "height": 914, 65 | "highlighted": false, 66 | "id": 3632, 67 | "incognito": false, 68 | "index": 2, 69 | "mutedInfo": { 70 | "muted": false 71 | }, 72 | "pinned": false, 73 | "selected": false, 74 | "status": "complete", 75 | "title": "https://github.com/deanoemcke/thegreatsuspender/issues/266", 76 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Fgithub.com%2Fdeanoemcke%2Fthegreatsuspender%2Fissues%2F266&pos=0&uri=https://github.com/deanoemcke/thegreatsuspender/issues/266", 77 | "width": 1680, 78 | "windowId": 3628 79 | }, 80 | { 81 | "active": false, 82 | "audible": false, 83 | "autoDiscardable": true, 84 | "discarded": false, 85 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAGB0lEQVRYR8WXbWgUVxSG37l7M7k7DUsMEtM0aKLpIiGVyqqxXZWtWWtitFJKFG3BftCm9oMQioT+kLaEImLbkIoULUGltFJsmx9+1EJq/dGIFYIlSFqCpGmIaZqWNQ3rZnYyuVPOzM5mN7ubCC04hOzs3Jn7Puc95565q+A+H8p91sc9A1jPh8SdaV+A+bylYJ7liiWFCy9NCWtG6pZhDCrx6dFF2lSvcuqKfi/BLQjwWygkvIs8YY8nL8BUDiZUZ17OZ+c3TUjDBKSE1A1IKWEZZu9UDN0VV+YHmRfgj9qaKo/w7rAsCJA45yAI+2BsFoCEUwBoQMYMSFPqbDp+7sGfbvTnciMnwO21a8PIV4IkautpicgJJFWcxKQECCBxOOLOdzpnltLz0I0b3dkgsgLcXr06LC0ryFQGitwGmAuSMpstoqmJqBMghkkOAPQXM2DC6lkx8EsGRAbA70srqyDUxmTEjMQTdrv2pwBRpARnR5xwwRV2HbA/6T5Tnl02fCstHWkAoR9C4pEfjebXOseFpjqWJ0HsLykwc/zMJuoKz96q6JDxjoqhoeQKSQPYcWHbdgsy8GzHMB4dMEAQboTuJK7VKCsBioudy8YYEBubg2RkrTuPF71lXYPn3cEkAEVfEBOtNOAbieHNIyMoNBlESsHZdfD4erD9TYgULHaKjAtozISGccjhDnB9Ns1clVBVpyYYo3MGqerQCmOHlQrYLiQB6i/WBz2WFXbJNp4ewZNXoxCcJyFYXR0iza24PKjjz6gET9SGaUosKWAI+zWUTByEGv0mKci4Ca6mu8EUpVupnupJA3jq24ZGKWeqXAB1XMdL7w+iVGc2hBbahMjbbfj02gR0Q0IIDjUBYNjVLu1HmzYUodLYD2FeypoCumhZSj9fNXU2DaDhfH2roljJ9kqD/i9HsPNSxAYo/+4C2m8yDEcMW9jtBUJlNhD1AgJZXizwVsiAb6wmJ4AZz9fz1/xzOA1gW1fdOyzRa9wnZdTAzoO/orrEj8LPvkbLFwPQBAdnLBm9ey+JmwmII7sqURnbCm7ezAphGiryA5PvLQhANxR8P4b9/aWYPNSJ9otDINcp9+QAnZP7djFKCaoF+n5k70pUm8+A61f/OwDNsLVLQ/nrx9DWdQtqntOU1MTqSIUwyIFpifY9fqzhe3ICGDEN3prIvTlAYmXRErRsOordR6874nZbTnkZOR7Y/w3TxMl9K7GKPQ3ofRkOkP3S5JkA9We2tipCEXPrwJ2hc/Np7P6oD2N3DPAEANUCHZR7AjBNE5VLNJx5sRjaaCCn/XE9X/cF/0ovwi2f1zXmaajKBfBYaRB1hW9g39HLiBqZLZlacYEKHH95A9bwJnA9cxm60cenvP2Fodvpy3DLqS1BT74SZoIhF0TzqhZok360dF7H6EQsLcLSQg3HmzZAM0+hUrZljZ5yb5ocVpx3+zaPpjei0MkQtZZWLgDum5vf2fnWFq/D3rJ9GI8wXBsYtwfW+4tRXCTx1c/H8ErxCRQm2m8qhSPOQC5MQD1c8YTzQkp7GdWeqN2ueD0BSvF8EPRgua8cApqtMRmNYnBsCAfKbiJcEsmInsTp0GMC8ZjoLWnI8jKiG8gFNp3XzLyKUAscF3KlI1XFnJTwswgO+fsheKIxUHEaTmczdNU+1++quv4AOtzoMxygCxs/rq3K8yqNVAvkxHw14UJQx/ywrA9+XzTJ5RYc5dw0OPSYCuMuP7ts1zwbEvfp9e3hsFaAIO2EqCYIIpcb0gAaxDBeLRtMRkzr3M23naK/NZiS9azYPbDwliwVgnMZVGlVpLjhjlNqSNwndXyyuA9CwBalBkUROzlXYcR4TvGsKUjNbc0HtVUexdrBBQRXmbM3pAJN7MxpG3igaAjrtFnrSdTOOwkbHl1aM+cefi7d9lSNBX+YlL8bEkUehFWNBbjq/BwgGDqqeQytRaMpeXetB4wZ1lug6N0VL8zu/zKWx9xlmO0G9xqBLFZnApZkpflez3IJiDbfOJaq9BuA0SZDZ1AGgZnRfE+8dyFhd94FHZgP6v8Yu+8A/wKjtIs/vGW6ZwAAAABJRU5ErkJggg==", 86 | "height": 914, 87 | "highlighted": false, 88 | "id": 3633, 89 | "incognito": false, 90 | "index": 3, 91 | "mutedInfo": { 92 | "muted": false 93 | }, 94 | "pinned": false, 95 | "selected": false, 96 | "status": "complete", 97 | "title": "chrome.i18n - Google Chrome", 98 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Fdeveloper.chrome.com%2Fextensions%2Fi18n&pos=0&uri=https://developer.chrome.com/extensions/i18n", 99 | "width": 1680, 100 | "windowId": 3628 101 | }, 102 | { 103 | "active": false, 104 | "audible": false, 105 | "autoDiscardable": true, 106 | "discarded": false, 107 | "favIconUrl": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB4klEQVRYR+1XPUgcQRT+3o6rHkg8JChyIVUKo0Uqr4hFFASx0O5AEEJMfcKdB6mUrIiNoCd4jSB2doKChaU/WF2dgFU6IQlKjCieuDMvzK6nrtzdHriwKXaq95jvvffNt28es4SQF4VcHxGBSAHiVErs73wbMIjfKUZL3U2ZGT9wsMsbH6rFEITiRAeLoSShNU4sJSmWEoTzJtPYej8/fUJ7zW8HAe6ru3AZmB53rcKGb2hj9xvEsxMOzi6VcPnzN+y/lzAlrdFBrCvnnHy0H3id8E12X/ApgbL/OEPpBljbBIwGtK/OenJrEle/zv7QXnPXV2ennEApQEnAtgGlN7R/F2sAWN92nWcSUErh7PuxeiDgf3YvwucTaK5l3lV75FUn01FLd8+tVCNgNLlBJAkkQSwZpAyAGYoBwQwWxMoEhMmZscNaTahzMQRrDEN6rjvDZKOzzebB3h/RHKCPnz67TRjSCp9AOpcbhuSkRwBBxcLi4m4lUYLGUza7FJN8MqnAMV3QAF0LSqzk81PXlQgEjXduwWT2S5LZHtY2UcPuSn6hWKslgsQ7BCzLMk7PL2a0/TL+Ys6yrJozJEj8/RxIZ6ac21BYXvIO7SpSBIWPCEQKRApECvw/CoT0HAj/5/QffopgMc7Z6fsAAAAASUVORK5CYII=", 108 | "height": 914, 109 | "highlighted": false, 110 | "id": 3634, 111 | "incognito": false, 112 | "index": 4, 113 | "mutedInfo": { 114 | "muted": false 115 | }, 116 | "pinned": false, 117 | "selected": false, 118 | "status": "complete", 119 | "title": "447856 - Feature Request: Ability for an extension to change the URL in the omnibox - chromium - Monorail", 120 | "url": "chrome-extension://jmhiginckkdplecbbnnfcapnpofhmgbb/suspended.html#ttl=https%3A%2F%2Fbugs.chromium.org%2Fp%2Fchromium%2Fissues%2Fdetail%3Fid%3D447856&pos=0&uri=https://bugs.chromium.org/p/chromium/issues/detail?id=447856", 121 | "width": 1680, 122 | "windowId": 3628 123 | } 124 | ], 125 | "top": 23, 126 | "type": "normal", 127 | "width": 1680 128 | } 129 | ], 130 | "date": "2018-08-29T14:07:04.041Z", 131 | "name": "savedSession1", 132 | "id": 33 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/js/tests/test_createAndUpdateSessionRestorePoint.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsIndexedDb, gsSession, getFixture, loadJsFile, assertTrue, FIXTURE_CURRENT_SESSIONS */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const oldVersion = '1.2.34'; 8 | 9 | const tests = [ 10 | // Test create session restore point. Should create a session from the currently open windows 11 | async () => { 12 | // Simulate gsSession.prepareForUpdate 13 | const session1 = await getFixture( 14 | FIXTURE_CURRENT_SESSIONS, 15 | 'currentSession1' 16 | ); 17 | const currentSession = await gsSession.buildCurrentSession(); 18 | currentSession.windows = session1.windows; 19 | const sessionRestorePointAfter = await gsIndexedDb.createOrUpdateSessionRestorePoint( 20 | currentSession, 21 | oldVersion 22 | ); 23 | const isSessionRestorePointValid = 24 | sessionRestorePointAfter.windows[0].tabs.length === 5; 25 | return assertTrue(isSessionRestorePointValid); 26 | }, 27 | 28 | // Test create session restore point when session restore point already exists from same session 29 | async () => { 30 | // Create a session restore point 31 | const session1 = await getFixture( 32 | FIXTURE_CURRENT_SESSIONS, 33 | 'currentSession1' 34 | ); 35 | const currentSession1 = await gsSession.buildCurrentSession(); 36 | currentSession1.windows = session1.windows; 37 | await gsIndexedDb.createOrUpdateSessionRestorePoint( 38 | currentSession1, 39 | oldVersion 40 | ); 41 | const newSessionRestorePointBefore = await gsIndexedDb.fetchSessionBySessionId( 42 | currentSession1.sessionId 43 | ); 44 | const isSessionRestorePointBeforeValid = 45 | newSessionRestorePointBefore.windows[0].tabs.length === 5; 46 | 47 | const session2 = await getFixture( 48 | FIXTURE_CURRENT_SESSIONS, 49 | 'currentSession1' 50 | ); 51 | const currentSession2 = await gsSession.buildCurrentSession(); 52 | currentSession2.windows = session2.windows; 53 | currentSession2.windows[0].tabs.push({ 54 | id: 7777, 55 | title: 'testTab', 56 | url: 'https://test.com', 57 | }); 58 | const sessionRestorePointAfter = await gsIndexedDb.createOrUpdateSessionRestorePoint( 59 | currentSession2, 60 | oldVersion 61 | ); 62 | const sessionRestorePointAfterValid = 63 | sessionRestorePointAfter.windows[0].tabs.length === 6; 64 | 65 | const gsTestDb = await gsIndexedDb.getDb(); 66 | const sessionRestoreCount = await gsTestDb 67 | .query(gsIndexedDb.DB_SAVED_SESSIONS) 68 | .filter(gsIndexedDb.DB_SESSION_PRE_UPGRADE_KEY, oldVersion) 69 | .execute() 70 | .then(o => o.length); 71 | 72 | return assertTrue( 73 | isSessionRestorePointBeforeValid && 74 | sessionRestorePointAfterValid && 75 | sessionRestoreCount === 1 76 | ); 77 | }, 78 | 79 | // Test create session restore point when session restore point already exists from another session 80 | async () => { 81 | // Create a session restore point (uses current session based on gsSession.getSessionId) 82 | const session1 = await getFixture( 83 | FIXTURE_CURRENT_SESSIONS, 84 | 'currentSession1' 85 | ); 86 | const currentSession1 = await gsSession.buildCurrentSession(); 87 | currentSession1.windows = session1.windows; 88 | const oldCurrentSessionId = currentSession1.sessionId; 89 | const sessionRestorePointBefore = await gsIndexedDb.createOrUpdateSessionRestorePoint( 90 | currentSession1, 91 | oldVersion 92 | ); 93 | const isSessionRestorePointBeforeValid = 94 | sessionRestorePointBefore.windows[0].tabs.length === 5; 95 | 96 | // Simulate an extension restart by resetting gsSession sessionId and saving a new 'current session' 97 | await loadJsFile('gsSession'); 98 | const session2 = await getFixture( 99 | FIXTURE_CURRENT_SESSIONS, 100 | 'currentSession1' 101 | ); 102 | const currentSession2 = await gsSession.buildCurrentSession(); 103 | const newCurrentSessionId = currentSession2.sessionId; 104 | const isCurrentSessionIdChanged = 105 | oldCurrentSessionId !== newCurrentSessionId; 106 | 107 | currentSession2.windows = session2.windows; 108 | currentSession2.windows[0].tabs.push({ 109 | id: 7777, 110 | title: 'testTab', 111 | url: 'https://test.com', 112 | }); 113 | const sessionRestorePointAfter = await gsIndexedDb.createOrUpdateSessionRestorePoint( 114 | currentSession2, 115 | oldVersion 116 | ); 117 | const sessionRestorePointAfterValid = 118 | sessionRestorePointAfter.windows[0].tabs.length === 6; 119 | 120 | const gsTestDb = await gsIndexedDb.getDb(); 121 | const sessionRestoreCount = await gsTestDb 122 | .query(gsIndexedDb.DB_SAVED_SESSIONS) 123 | .filter(gsIndexedDb.DB_SESSION_PRE_UPGRADE_KEY, oldVersion) 124 | .execute() 125 | .then(o => o.length); 126 | 127 | return assertTrue( 128 | isSessionRestorePointBeforeValid && 129 | isCurrentSessionIdChanged && 130 | sessionRestorePointAfterValid && 131 | sessionRestoreCount === 1 132 | ); 133 | }, 134 | ]; 135 | 136 | return { 137 | name: 'Session Restore Points', 138 | tests, 139 | }; 140 | })() 141 | ); 142 | -------------------------------------------------------------------------------- /src/js/tests/test_currentSessions.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsIndexedDb, gsSession, getFixture, assertTrue, FIXTURE_CURRENT_SESSIONS */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const tests = [ 8 | // Test saving new currentSession 9 | async () => { 10 | const currentSessionId = gsSession.getSessionId(); 11 | const currentSessionsBefore = await gsIndexedDb.fetchCurrentSessions(); 12 | const wasCurrentSessionsEmpty = currentSessionsBefore.length === 0; 13 | 14 | // Simulate gsSession.updateCurrentSession() 15 | const session1 = await getFixture( 16 | FIXTURE_CURRENT_SESSIONS, 17 | 'currentSession1' 18 | ); 19 | const currentSession = await gsSession.buildCurrentSession(); 20 | currentSession.windows = session1.windows; 21 | await gsIndexedDb.updateSession(currentSession); 22 | const savedCurrentSession = await gsIndexedDb.fetchSessionBySessionId( 23 | currentSessionId 24 | ); 25 | 26 | const isSessionValid = 27 | savedCurrentSession.sessionId === currentSessionId && 28 | savedCurrentSession.windows.length === 1 && 29 | savedCurrentSession.windows[0].tabs.length === 5 && 30 | savedCurrentSession.windows[0].tabs[0].id === 3630; 31 | 32 | const currentSessionsAfter = await gsIndexedDb.fetchCurrentSessions(); 33 | const isCurrentSessionsPopulated = currentSessionsAfter.length === 1; 34 | 35 | return assertTrue( 36 | wasCurrentSessionsEmpty && 37 | isCurrentSessionsPopulated && 38 | isSessionValid 39 | ); 40 | }, 41 | 42 | // Test updating existing currentSession 43 | async () => { 44 | const currentSessionId = gsSession.getSessionId(); 45 | const session1 = await getFixture( 46 | FIXTURE_CURRENT_SESSIONS, 47 | 'currentSession1' 48 | ); 49 | const currentSession1 = await gsSession.buildCurrentSession(); 50 | currentSession1.windows = session1.windows; 51 | await gsIndexedDb.updateSession(currentSession1); 52 | const dbCurrentSession1 = await gsIndexedDb.fetchSessionBySessionId( 53 | currentSessionId 54 | ); 55 | const isSession1Valid = 56 | dbCurrentSession1.sessionId === currentSessionId && 57 | dbCurrentSession1.windows[0].tabs.length === 5; 58 | 59 | const session2 = await getFixture( 60 | FIXTURE_CURRENT_SESSIONS, 61 | 'currentSession1' 62 | ); 63 | const currentSession2 = await gsSession.buildCurrentSession(); 64 | currentSession2.windows = session2.windows; 65 | currentSession2.windows[0].tabs.push({ 66 | id: 7777, 67 | title: 'testTab', 68 | url: 'https://test.com', 69 | }); 70 | await gsIndexedDb.updateSession(currentSession2); 71 | 72 | const dbCurrentSession2 = await gsIndexedDb.fetchSessionBySessionId( 73 | currentSessionId 74 | ); 75 | const isSession2Valid = 76 | dbCurrentSession2.sessionId === currentSessionId && 77 | dbCurrentSession2.windows.length === 1 && 78 | dbCurrentSession2.windows[0].tabs.length === 6 && 79 | dbCurrentSession2.windows[0].tabs[5].id === 7777 && 80 | dbCurrentSession1.id === dbCurrentSession2.id && 81 | dbCurrentSession1.date < dbCurrentSession2.date; 82 | 83 | const currentSessionsAfter = await gsIndexedDb.fetchCurrentSessions(); 84 | const isCurrentSessionsAfterValid = currentSessionsAfter.length === 1; 85 | 86 | return assertTrue( 87 | isSession1Valid && isSession2Valid && isCurrentSessionsAfterValid 88 | ); 89 | }, 90 | ]; 91 | 92 | return { 93 | name: 'Current Sessions', 94 | tests, 95 | }; 96 | })() 97 | ); 98 | -------------------------------------------------------------------------------- /src/js/tests/test_gsTabQueue.js: -------------------------------------------------------------------------------- 1 | /*global chrome, GsTabQueue, gsUtils, assertTrue */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const MAX_REQUEUES = 2; 8 | 9 | function buildExecutorResolveTrue(executorDelay) { 10 | return async (tab, executionProps, resolve, reject, requeue) => { 11 | await gsUtils.setTimeout(executorDelay); 12 | resolve(true); 13 | }; 14 | } 15 | 16 | function buildExecutorRequeue(executorDelay, requeueDelay) { 17 | return async (tab, executionProps, resolve, reject, requeue) => { 18 | executionProps.requeues = executionProps.requeues || 0; 19 | await gsUtils.setTimeout(executorDelay); 20 | if (executionProps.requeues !== MAX_REQUEUES) { 21 | executionProps.requeues += 1; 22 | requeue(requeueDelay); 23 | } else { 24 | resolve(true); 25 | } 26 | }; 27 | } 28 | 29 | function buildExceptionResolvesFalse() { 30 | return (tab, executionProps, exceptionType, resolve, reject, requeue) => { 31 | resolve(false); 32 | }; 33 | } 34 | function buildExceptionRejects() { 35 | return (tab, executionProps, exceptionType, resolve, reject, requeue) => { 36 | reject('Test error'); 37 | }; 38 | } 39 | 40 | async function runQueueTest(tabCount, gsTabQueue) { 41 | const tabCheckPromises = []; 42 | for (let tabId = 1; tabId <= tabCount; tabId += 1) { 43 | const tabCheckPromise = gsTabQueue.queueTabAsPromise({ 44 | id: tabId, 45 | }); 46 | tabCheckPromises.push(tabCheckPromise); 47 | } 48 | 49 | let results; 50 | try { 51 | results = await Promise.all(tabCheckPromises); 52 | } catch (e) { 53 | console.log('Error!', e); 54 | } 55 | 56 | // Wait for queue to finish 57 | while (gsTabQueue.getTotalQueueSize() > 0) { 58 | await gsUtils.setTimeout(10); 59 | } 60 | return results; 61 | } 62 | 63 | async function makeTest( 64 | tabCount, 65 | executorDelay, 66 | requeueDelay, 67 | concurrentExecutors, 68 | jobTimeout, 69 | executorFnType, 70 | exceptionFnType, 71 | shouldGenerateException, 72 | expectedTimeTaken 73 | ) { 74 | let executorFn; 75 | if (executorFnType === 'resolveTrue') { 76 | executorFn = buildExecutorResolveTrue(executorDelay); 77 | } else if (executorFnType === 'requeue') { 78 | executorFn = buildExecutorRequeue(executorDelay, requeueDelay); 79 | } 80 | let exceptionFn; 81 | if (exceptionFnType === 'resolveFalse') { 82 | exceptionFn = buildExceptionResolvesFalse(); 83 | } else if (exceptionFnType === 'reject') { 84 | exceptionFn = buildExceptionRejects(); 85 | } 86 | const queueProps = { 87 | concurrentExecutors, 88 | jobTimeout, 89 | executorFn, 90 | exceptionFn, 91 | processingDelay: 0, 92 | }; 93 | 94 | const startTime = Date.now(); 95 | const gsTabQueue = GsTabQueue('testQueue', queueProps); 96 | const results = await runQueueTest(tabCount, gsTabQueue); 97 | const timeTaken = Date.now() - startTime; 98 | console.log( 99 | `timers. timeTaken: ${timeTaken}. expected: ${expectedTimeTaken}` 100 | ); 101 | 102 | let isResultsValid = false; 103 | if (shouldGenerateException && exceptionFnType === 'resolveFalse') { 104 | isResultsValid = 105 | results.length === tabCount && results.every(o => o === false); 106 | } else if (shouldGenerateException && exceptionFnType === 'reject') { 107 | isResultsValid = typeof results === 'undefined'; 108 | } else { 109 | isResultsValid = 110 | results.length === tabCount && results.every(o => o === true); 111 | } 112 | 113 | // Nasty hack here 114 | const allowedTimingVariation = 150; 115 | 116 | let isTimeValid = 117 | timeTaken > expectedTimeTaken && 118 | timeTaken < expectedTimeTaken + allowedTimingVariation; 119 | 120 | return assertTrue(isResultsValid && isTimeValid); 121 | } 122 | 123 | const tests = [ 124 | async () => { 125 | // Test: 5 tabs. 100ms per tab. No requeue delay. 1 at a time. 1000ms timeout. 126 | // Executor function resolvesTrue. Exception function resolvesFalse. 127 | // Should resolveTrue. 128 | // Expected time taken: 5 * 100 + 5 * 50 129 | return await makeTest( 130 | 5, 131 | 100, 132 | 0, 133 | 1, 134 | 1000, 135 | 'resolveTrue', 136 | 'resolveFalse', 137 | false, 138 | 750 139 | ); 140 | }, 141 | 142 | async () => { 143 | // Test: 5 tabs. 100ms per tab. No requeue delay. 2 at a time. 1000ms timeout. 144 | // Executor function resolvesTrue. Exception function resolvesFalse. 145 | // Should resolveTrue. 146 | // Expected time taken: 3 * 100 + 3 * 50 147 | return await makeTest( 148 | 5, 149 | 100, 150 | 0, 151 | 2, 152 | 1000, 153 | 'resolveTrue', 154 | 'resolveFalse', 155 | false, 156 | 450 157 | ); 158 | }, 159 | 160 | async () => { 161 | // Test: 5 tabs. 100ms per tab. No requeue delay. 50 at a time. 1000ms timeout. 162 | // Executor function resolvesTrue. Exception function resolvesFalse. 163 | // Should resolveTrue. 164 | // Expected time taken: 1 * 100 + 1 * 50 165 | return await makeTest( 166 | 5, 167 | 100, 168 | 0, 169 | 50, 170 | 1000, 171 | 'resolveTrue', 172 | 'resolveFalse', 173 | false, 174 | 150 175 | ); 176 | }, 177 | 178 | async () => { 179 | // Test: 50 tabs. 100ms per tab. No requeue delay. 20 at a time. 1000ms timeout. 180 | // Executor function resolvesTrue. Exception function resolvesFalse. 181 | // Should resolveTrue. 182 | // Expected time taken: 3 * 100 + 3 * 50 183 | return await makeTest( 184 | 50, 185 | 100, 186 | 0, 187 | 20, 188 | 1000, 189 | 'resolveTrue', 190 | 'resolveFalse', 191 | false, 192 | 450 193 | ); 194 | }, 195 | 196 | async () => { 197 | // Test: 5 tabs. 100ms per tab. No requeue delay. 1 at a time. 10ms timeout. 198 | // Executor function resolvesTrue. Exception function resolvesFalse. 199 | // Should timeout on each execution. 200 | // Should resolveFalse. 201 | // Expected time taken: 5 * 10 + 5 * 50 202 | return await makeTest( 203 | 5, 204 | 100, 205 | 0, 206 | 1, 207 | 10, 208 | 'resolveTrue', 209 | 'resolveFalse', 210 | true, 211 | 300 212 | ); 213 | }, 214 | 215 | async () => { 216 | // Test: 5 tabs. 100ms per tab. No requeue delay. 1 at a time. 10ms timeout. 217 | // Executor function resolvesTrue. Exception function rejects. 218 | // Results should be undefined as Promises.all rejects. 219 | // Should reject. 220 | // Expected time taken: 5 * 10 + 5 * 50 221 | return await makeTest(5, 100, 0, 1, 10, 'resolveTrue', 'reject', true, 300); 222 | }, 223 | 224 | async () => { 225 | // Test: 1 tab. 100ms per tab. 100ms requeue delay, 1 at a time. 1000ms timeout. 226 | // Executor function requeues (up to 2 times). 227 | // Exception function resolvesFalse. 228 | // Should requeue 2 times then resolveTrue. 229 | // Expected time taken: 3 * 1 * 100 + 3 * 100 + 1 * 50 230 | return await makeTest( 231 | 1, 232 | 100, 233 | 100, 234 | 1, 235 | 1000, 236 | 'requeue', 237 | 'resolveFalse', 238 | false, 239 | 650 240 | ); 241 | }, 242 | 243 | async () => { 244 | // Test: 1 tab. 100ms per tab. 100ms requeue delay, 1 at a time. 250ms timeout. 245 | // Executor function requeues (up to 2 times). 246 | // Exception function resolvesFalse. 247 | // Should requeue up to 2 times then timeout. 248 | // Expected time taken: 1 * 250 + 1 * 50 249 | return await makeTest( 250 | 1, 251 | 100, 252 | 100, 253 | 1, 254 | 250, 255 | 'requeue', 256 | 'resolveFalse', 257 | true, 258 | 300 259 | ); 260 | }, 261 | 262 | async () => { 263 | // Test: 1 tab. 100ms per tab. 100ms requeue delay. 1 at a time. 250ms timeout. 264 | // Executor function requeues (up to 2 times). 265 | // Exception function rejects. 266 | // Should requeue up to 2 times then timeout. 267 | // Expected time taken: 1 * 250 + 1 * 50 268 | return await makeTest(1, 100, 100, 1, 250, 'requeue', 'reject', true, 300); 269 | }, 270 | ]; 271 | 272 | return { 273 | name: 'gsTabQueue Library', 274 | tests, 275 | }; 276 | })() 277 | ); 278 | -------------------------------------------------------------------------------- /src/js/tests/test_gsUtils.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsUtils, assertTrue */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const tests = [ 8 | // Test gsUtils.setTimeout 9 | async () => { 10 | const timeout = 500; 11 | const timeBefore = new Date().getTime(); 12 | await gsUtils.setTimeout(timeout); 13 | const timeAfter = new Date().getTime(); 14 | const isTimeAfterValid = 15 | timeAfter > timeBefore + timeout && 16 | timeAfter < timeBefore + timeout + 200; 17 | 18 | return assertTrue(isTimeAfterValid); 19 | }, 20 | 21 | // Test gsUtils.getRootUrl 22 | async () => { 23 | const rawUrl1 = 'https://searchengine.site'; 24 | const isUrl1Valid = 25 | gsUtils.getRootUrl(rawUrl1, false, false) === 'searchengine.site' && 26 | gsUtils.getRootUrl(rawUrl1, true, false) === 'searchengine.site' && 27 | gsUtils.getRootUrl(rawUrl1, false, true) === 'https://searchengine.site' && 28 | gsUtils.getRootUrl(rawUrl1, true, true) === 'https://searchengine.site'; 29 | 30 | const rawUrl2 = 'https://searchengine.site/'; 31 | const isUrl2Valid = 32 | gsUtils.getRootUrl(rawUrl2, false, false) === 'searchengine.site' && 33 | gsUtils.getRootUrl(rawUrl2, true, false) === 'searchengine.site' && 34 | gsUtils.getRootUrl(rawUrl2, false, true) === 'https://searchengine.site' && 35 | gsUtils.getRootUrl(rawUrl2, true, true) === 'https://searchengine.site'; 36 | 37 | const rawUrl3 = 38 | 'https://searchengine.site/search?source=hp&ei=T2HWW9jfGoWJ5wKzt4WgBw&q=rabbits&oq=rabbits&gs_l=psy-ab.3..35i39k1l2j0i67k1l6j0i10k1j0i67k1.1353.2316.0.2448.9.7.0.0.0.0.120.704.4j3.7.0....0...1c.1.64.psy-ab..2.7.701.0..0.0.dk-gx_j1MUI'; 39 | const isUrl3Valid = 40 | gsUtils.getRootUrl(rawUrl3, false, false) === 'searchengine.site' && 41 | gsUtils.getRootUrl(rawUrl3, true, false) === 'searchengine.site/search' && 42 | gsUtils.getRootUrl(rawUrl3, false, true) === 'https://searchengine.site' && 43 | gsUtils.getRootUrl(rawUrl3, true, true) === 44 | 'https://searchengine.site/search'; 45 | 46 | const rawUrl4 = 'www.searchengine.site'; 47 | const isUrl4Valid = 48 | gsUtils.getRootUrl(rawUrl4, false, false) === 'www.searchengine.site' && 49 | gsUtils.getRootUrl(rawUrl4, true, false) === 'www.searchengine.site' && 50 | gsUtils.getRootUrl(rawUrl4, false, true) === 'www.searchengine.site' && 51 | gsUtils.getRootUrl(rawUrl4, true, true) === 'www.searchengine.site'; 52 | 53 | const rawUrl5 = 54 | 'https://github.com/torvalds/linux/issues/478#issuecomment-430780678'; 55 | const isUrl5Valid = 56 | gsUtils.getRootUrl(rawUrl5, false, false) === 'github.com' && 57 | gsUtils.getRootUrl(rawUrl5, true, false) === 58 | 'github.com/torvalds/linux/issues/478' && 59 | gsUtils.getRootUrl(rawUrl5, false, true) === 'https://github.com' && 60 | gsUtils.getRootUrl(rawUrl5, true, true) === 61 | 'https://github.com/torvalds/linux/issues/478'; 62 | 63 | const rawUrl6 = 'file:///Users/username/Downloads/session%20(63).txt'; 64 | const isUrl6Valid = 65 | gsUtils.getRootUrl(rawUrl6, false, false) === 66 | '/Users/username/Downloads' && 67 | gsUtils.getRootUrl(rawUrl6, true, false) === 68 | '/Users/username/Downloads/session%20(63).txt' && 69 | gsUtils.getRootUrl(rawUrl6, false, true) === 70 | 'file:///Users/username/Downloads' && 71 | gsUtils.getRootUrl(rawUrl6, true, true) === 72 | 'file:///Users/username/Downloads/session%20(63).txt'; 73 | 74 | const rawUrl7 = 75 | 'https://subdomain.domain.org/serverdir/web/#/report-home/a52338347w84781065p87884368'; 76 | const isUrl7Valid = 77 | gsUtils.getRootUrl(rawUrl7, false, false) === 78 | 'subdomain.domain.org' && 79 | gsUtils.getRootUrl(rawUrl7, true, false) === 80 | 'subdomain.domain.org/serverdir/web' && 81 | gsUtils.getRootUrl(rawUrl7, false, true) === 82 | 'https://subdomain.domain.org' && 83 | gsUtils.getRootUrl(rawUrl7, true, true) === 84 | 'https://subdomain.domain.org/serverdir/web'; 85 | 86 | return assertTrue( 87 | isUrl1Valid && 88 | isUrl2Valid && 89 | isUrl3Valid && 90 | isUrl4Valid && 91 | isUrl5Valid && 92 | isUrl6Valid && 93 | isUrl7Valid 94 | ); 95 | }, 96 | 97 | // Test gsUtils.executeWithRetries 98 | async () => { 99 | const successPromiseFn = val => new Promise((r, j) => r(val)); 100 | let result1; 101 | const timeBefore1 = new Date().getTime(); 102 | try { 103 | result1 = await gsUtils.executeWithRetries( 104 | successPromiseFn, 105 | 'a', 106 | 3, 107 | 500 108 | ); 109 | } catch (e) { 110 | // do nothing 111 | } 112 | const timeAfter1 = new Date().getTime(); 113 | const timeTaken1 = timeAfter1 - timeBefore1; 114 | const isTime1Valid = timeTaken1 >= 0 && timeTaken1 < 100; 115 | const isResult1Valid = result1 === 'a'; 116 | 117 | const errorPromiseFn = val => new Promise((r, j) => j()); 118 | let result2; 119 | const timeBefore2 = new Date().getTime(); 120 | try { 121 | result2 = await gsUtils.executeWithRetries( 122 | errorPromiseFn, 123 | 'b', 124 | 3, 125 | 500 126 | ); 127 | } catch (e) { 128 | // do nothing 129 | } 130 | const timeAfter2 = new Date().getTime(); 131 | const timeTaken2 = timeAfter2 - timeBefore2; 132 | const isTime2Valid = timeTaken2 >= 1500 && timeTaken2 < 1600; 133 | const isResult2Valid = result2 === undefined; 134 | 135 | return assertTrue( 136 | isResult1Valid && isTime1Valid && isResult2Valid && isTime2Valid 137 | ); 138 | }, 139 | ]; 140 | 141 | return { 142 | name: 'gsUtils Library', 143 | tests, 144 | }; 145 | })() 146 | ); 147 | -------------------------------------------------------------------------------- /src/js/tests/test_savedSessions.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsIndexedDb, getFixture, assertTrue, FIXTURE_SAVED_SESSIONS */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const tests = [ 8 | // Test saving new savedSession 9 | async () => { 10 | const currentSessionsBefore = await gsIndexedDb.fetchCurrentSessions(); 11 | const wasCurrentSessionsEmpty = currentSessionsBefore.length === 0; 12 | const savedSessionsBefore = await gsIndexedDb.fetchSavedSessions(); 13 | const wasSavedSessionsEmpty = savedSessionsBefore.length === 0; 14 | 15 | const session1 = await getFixture( 16 | FIXTURE_SAVED_SESSIONS, 17 | 'savedSession1' 18 | ); 19 | await gsIndexedDb.updateSession(session1); 20 | const dbSavedSession1 = await gsIndexedDb.fetchSessionBySessionId( 21 | session1.sessionId 22 | ); 23 | const isSessionValid = 24 | dbSavedSession1.id === session1.id && 25 | dbSavedSession1.sessionId === session1.sessionId && 26 | dbSavedSession1.sessionId.indexOf('_') === 0 && 27 | dbSavedSession1.windows.length === 1 && 28 | dbSavedSession1.windows[0].tabs.length === 5 && 29 | dbSavedSession1.windows[0].tabs[0].id === 3630; 30 | 31 | const currentSessionsAfter = await gsIndexedDb.fetchCurrentSessions(); 32 | const isCurrentSessionsEmpty = currentSessionsAfter.length === 0; 33 | 34 | const savedSessionsAfter = await gsIndexedDb.fetchSavedSessions(); 35 | const isSavedSessionsPopulated = savedSessionsAfter.length === 1; 36 | 37 | return assertTrue( 38 | wasCurrentSessionsEmpty && 39 | wasSavedSessionsEmpty && 40 | isCurrentSessionsEmpty && 41 | isSavedSessionsPopulated && 42 | isSessionValid 43 | ); 44 | }, 45 | 46 | // Test removing savedSession 47 | async () => { 48 | const session1 = await getFixture( 49 | FIXTURE_SAVED_SESSIONS, 50 | 'savedSession1' 51 | ); 52 | await gsIndexedDb.updateSession(session1); 53 | const savedSessionsBefore = await gsIndexedDb.fetchSavedSessions(); 54 | const isSavedSessionsBeforeValid = savedSessionsBefore.length === 1; 55 | 56 | await gsIndexedDb.removeSessionFromHistory( 57 | savedSessionsBefore[0].sessionId 58 | ); 59 | 60 | const savedSessionsAfter = await gsIndexedDb.fetchSavedSessions(); 61 | const isSavedSessionsAfterValid = savedSessionsAfter.length === 0; 62 | 63 | return assertTrue( 64 | isSavedSessionsBeforeValid && isSavedSessionsAfterValid 65 | ); 66 | }, 67 | 68 | // Test saving a lot of large sessions 69 | // async () => { 70 | // const largeSessionTemplate = getFixture(FIXTURE_SAVED_SESSIONS, 'savedSession1'); 71 | // delete largeSessionTemplate.id; 72 | // const tabsTemplate = JSON.parse( 73 | // JSON.stringify(largeSessionTemplate.windows[0].tabs) 74 | // ); 75 | // for (let i = 0; i < 500; i++) { 76 | // largeSessionTemplate.windows[0].tabs = largeSessionTemplate.windows[0].tabs.concat( 77 | // JSON.parse(JSON.stringify(tabsTemplate)) 78 | // ); 79 | // } 80 | // for (let j = 0; j < 50; j++) { 81 | // const largeSession = JSON.parse(JSON.stringify(largeSessionTemplate)); 82 | // largeSession.sessionId = '_' + j; 83 | // const dbSession = await gsStorage.updateSession(largeSession); 84 | // } 85 | // 86 | // const savedSessionsAfter = await gsStorage.fetchSavedSessions(); 87 | // const isSavedSessionsPopulated = savedSessionsAfter.length === 101; 88 | // 89 | // return assertTrue( 90 | // isSavedSessionsPopulated 91 | // ); 92 | // }, 93 | ]; 94 | 95 | return { 96 | name: 'Saved Sessions', 97 | tests, 98 | }; 99 | })() 100 | ); 101 | -------------------------------------------------------------------------------- /src/js/tests/test_suspendTab.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsIndexedDb, gsTabSuspendManager, getFixture, assertTrue, FIXTURE_CURRENT_SESSIONS, FIXTURE_PREVIEW_URLS */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const tests = [ 8 | // Test functions associated with suspending a tab 9 | async () => { 10 | const session1 = await getFixture( 11 | FIXTURE_CURRENT_SESSIONS, 12 | 'currentSession1' 13 | ); 14 | const tab = session1.windows[0].tabs[0]; 15 | const previewUrl = await getFixture( 16 | FIXTURE_PREVIEW_URLS, 17 | 'previewUrl1' 18 | ); 19 | 20 | await gsTabSuspendManager.saveSuspendData(tab); 21 | const tabProperties = await gsIndexedDb.fetchTabInfo(tab.url); 22 | const isTabPropertiesValid = 23 | tabProperties.url === tab.url && 24 | tabProperties.title === tab.title && 25 | tabProperties.favIconUrl === tab.favIconUrl; 26 | 27 | await gsIndexedDb.addPreviewImage(tab.url, previewUrl); 28 | const preview = await gsIndexedDb.fetchPreviewImage(tab.url); 29 | const isPreviewValid = preview.img === previewUrl; 30 | 31 | return assertTrue(isTabPropertiesValid && isPreviewValid); 32 | }, 33 | ]; 34 | 35 | return { 36 | name: 'Suspend Tab', 37 | tests, 38 | }; 39 | })() 40 | ); 41 | -------------------------------------------------------------------------------- /src/js/tests/test_trimDbItems.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsIndexedDb, gsSession, getFixture, assertTrue, FIXTURE_CURRENT_SESSIONS */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const tests = [ 8 | // Test trim currentSessions 9 | async () => { 10 | const currentSessionId = gsSession.getSessionId(); 11 | // Simulate adding 10 older sessions in DB_CURRENT_SESSIONS 12 | for (let i = 10; i > 0; i--) { 13 | const oldSession = await getFixture( 14 | FIXTURE_CURRENT_SESSIONS, 15 | 'currentSession1' 16 | ); 17 | delete oldSession.id; 18 | oldSession.sessionId = i + ''; 19 | const previousDateInMs = Date.now() - 1000 * 60 * 60 * i; 20 | oldSession.date = new Date(previousDateInMs).toISOString(); 21 | await gsIndexedDb.updateSession(oldSession); 22 | } 23 | 24 | // Add a current session 25 | const session1 = await getFixture( 26 | FIXTURE_CURRENT_SESSIONS, 27 | 'currentSession1' 28 | ); 29 | const currentSession1 = await gsSession.buildCurrentSession(); 30 | currentSession1.windows = session1.windows; 31 | await gsIndexedDb.updateSession(currentSession1); 32 | 33 | const currentSessionsBefore = await gsIndexedDb.fetchCurrentSessions(); 34 | const areCurrentSessionsBeforeValid = 35 | currentSessionsBefore.length === 11; 36 | 37 | const lastSessionBefore = await gsIndexedDb.fetchLastSession(); 38 | const isLastSessionBeforeValid = lastSessionBefore.sessionId === '1'; 39 | 40 | await gsIndexedDb.trimDbItems(); 41 | 42 | // Ensure current session still exists 43 | const currentSession = await gsIndexedDb.fetchSessionBySessionId( 44 | currentSessionId 45 | ); 46 | const isCurrentSessionValid = currentSession !== null; 47 | 48 | // Ensure correct DB_CURRENT_SESSIONS items were trimmed 49 | const currentSessionsAfter = await gsIndexedDb.fetchCurrentSessions(); 50 | const areCurrentSessionsAfterValid = currentSessionsAfter.length === 5; 51 | 52 | // Ensure fetchLastSession returns correct session 53 | const lastSessionAfter = await gsIndexedDb.fetchLastSession(); 54 | const isLastSessionAfterValid = lastSessionAfter.sessionId === '1'; 55 | 56 | return assertTrue( 57 | areCurrentSessionsBeforeValid && 58 | isLastSessionBeforeValid && 59 | isCurrentSessionValid && 60 | areCurrentSessionsAfterValid && 61 | isLastSessionAfterValid 62 | ); 63 | }, 64 | ]; 65 | 66 | return { 67 | name: 'Trim Db Items', 68 | tests, 69 | }; 70 | })() 71 | ); 72 | -------------------------------------------------------------------------------- /src/js/tests/test_updateCurrentSession.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsSession, gsIndexedDb, gsUtils, getFixture, assertTrue, FIXTURE_CURRENT_SESSIONS */ 2 | var testSuites = typeof testSuites === 'undefined' ? [] : testSuites; 3 | testSuites.push( 4 | (function() { 5 | 'use strict'; 6 | 7 | const tests = [ 8 | // Test hammering current session with updates 9 | async () => { 10 | const currentSessionWindows = []; 11 | const currentSessionId = gsSession.getSessionId(); 12 | 13 | for (let i = 0; i < 100; i++) { 14 | const session1 = await getFixture( 15 | FIXTURE_CURRENT_SESSIONS, 16 | 'currentSession1' 17 | ); 18 | let windowTemplate = session1.windows[0]; 19 | windowTemplate.id = i; 20 | currentSessionWindows.push(windowTemplate); 21 | 22 | const currentSession = await gsSession.buildCurrentSession(); 23 | currentSession.windows = currentSessionWindows; 24 | 25 | // Purposely don't await on this call 26 | gsIndexedDb.updateSession(currentSession); 27 | await gsUtils.setTimeout(1); 28 | } 29 | 30 | //if it's a saved session (prefixed with an underscore) 31 | const gsTestDb = await gsIndexedDb.getDb(); 32 | const results = await gsTestDb 33 | .query(gsIndexedDb.DB_CURRENT_SESSIONS, 'sessionId') 34 | .only(currentSessionId) 35 | .desc() 36 | .execute(); 37 | const onlySingleSessionForIdExists = results.length === 1; 38 | 39 | const currentSessionsAfter = await gsIndexedDb.fetchCurrentSessions(); 40 | const isCurrentSessionsPopulated = currentSessionsAfter.length === 1; 41 | const isCurrentSessionValid = 42 | currentSessionsAfter[0].windows.length === 100; 43 | 44 | return assertTrue( 45 | onlySingleSessionForIdExists && 46 | isCurrentSessionsPopulated && 47 | isCurrentSessionValid 48 | ); 49 | }, 50 | ]; 51 | 52 | return { 53 | name: 'Update current session', 54 | tests, 55 | }; 56 | })() 57 | ); 58 | -------------------------------------------------------------------------------- /src/js/tests/tests.js: -------------------------------------------------------------------------------- 1 | /* global gsIndexedDb, testSuites */ 2 | /* eslint-disable no-unused-vars */ 3 | 4 | const FIXTURE_CURRENT_SESSIONS = 'currentSessions'; 5 | const FIXTURE_SAVED_SESSIONS = 'savedSessions'; 6 | const FIXTURE_PREVIEW_URLS = 'previewUrls'; 7 | 8 | const requiredLibs = [ 9 | 'db', 10 | 'gsSession', 11 | 'gsStorage', 12 | 'gsUtils', 13 | 'gsChrome', 14 | 'gsTabSuspendManager', 15 | 'gsIndexedDb', 16 | 'gsTabQueue', 17 | 'gsFavicon', 18 | ]; 19 | 20 | function loadJsFile(fileName) { 21 | return new Promise(resolve => { 22 | const script = document.createElement('script'); 23 | script.onload = resolve; 24 | script.src = chrome.extension.getURL(`js/${fileName}.js`); 25 | document.head.appendChild(script); 26 | }); 27 | } 28 | 29 | function loadJsonFixture(fileName) { 30 | return new Promise(resolve => { 31 | const request = new XMLHttpRequest(); 32 | request.open( 33 | 'GET', 34 | chrome.extension.getURL(`js/tests/fixture_${fileName}.json`), 35 | true 36 | ); 37 | request.onload = () => { 38 | return resolve(JSON.parse(request.responseText)); 39 | }; 40 | request.send(); 41 | }); 42 | } 43 | 44 | function assertTrue(testResult) { 45 | if (testResult) { 46 | return Promise.resolve(true); 47 | } else { 48 | return Promise.reject(new Error(Error.captureStackTrace({}))); 49 | } 50 | } 51 | 52 | async function getFixture(fixtureName, itemName) { 53 | const fixtures = await loadJsonFixture(fixtureName); 54 | return JSON.parse(JSON.stringify(fixtures[itemName])); 55 | } 56 | 57 | async function runTests() { 58 | for (let testSuite of testSuites) { 59 | const resultEl = document.createElement('div'); 60 | resultEl.innerHTML = `Testing ${testSuite.name}...`; 61 | document.getElementById('results').appendChild(resultEl); 62 | 63 | let allTestsPassed = true; 64 | console.log(`Running testSuite: ${testSuite.name}..`); 65 | for (let [j, test] of testSuite.tests.entries()) { 66 | console.log(` Running test ${j + 1}..`); 67 | 68 | // loads/reset required libs 69 | await Promise.all(requiredLibs.map(loadJsFile)); 70 | 71 | // clear indexedDb contents 72 | gsIndexedDb.DB_SERVER = 'tgsTest'; 73 | await gsIndexedDb.clearGsDatabase(); 74 | 75 | // run test 76 | try { 77 | const result = await test(); 78 | console.log(` ${result}`); 79 | allTestsPassed = allTestsPassed && result; 80 | } catch (e) { 81 | allTestsPassed = false; 82 | console.error(e); 83 | } 84 | } 85 | 86 | //update test.html with testSuite result 87 | if (allTestsPassed) { 88 | resultEl.innerHTML = `Testing ${testSuite.name}: PASSED`; 89 | resultEl.style = 'color: green;'; 90 | } else { 91 | resultEl.innerHTML = `Testing ${testSuite.name}: FAILED`; 92 | resultEl.style = 'color: red;'; 93 | } 94 | } 95 | document.getElementById('suspendy-guy-inprogress').style.display = 'none'; 96 | document.getElementById('suspendy-guy-complete').style.display = 97 | 'inline-block'; 98 | } 99 | 100 | if (document.readyState !== 'loading') { 101 | runTests(); 102 | } else { 103 | document.addEventListener('DOMContentLoaded', function() { 104 | runTests(); 105 | }); 106 | } 107 | -------------------------------------------------------------------------------- /src/js/update.js: -------------------------------------------------------------------------------- 1 | /*global chrome, historyUtils, gsSession, gsIndexedDb, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | function setRestartExtensionClickHandler(warnFirst) { 13 | document.getElementById('restartExtensionBtn').onclick = async function(e) { 14 | // var result = true; 15 | // if (warnFirst) { 16 | // result = window.confirm(chrome.i18n.getMessage('js_update_confirm')); 17 | // } 18 | // if (result) { 19 | 20 | document.getElementById('restartExtensionBtn').className += ' btnDisabled'; 21 | document.getElementById('restartExtensionBtn').onclick = null; 22 | 23 | const currentSession = await gsSession.buildCurrentSession(); 24 | if (currentSession) { 25 | var currentVersion = chrome.runtime.getManifest().version; 26 | await gsIndexedDb.createOrUpdateSessionRestorePoint( 27 | currentSession, 28 | currentVersion 29 | ); 30 | } 31 | 32 | //ensure we don't leave any windows with no unsuspended tabs 33 | await gsSession.unsuspendActiveTabInEachWindow(); 34 | 35 | //update current session to ensure the new tab ids are saved before 36 | //we restart the extension 37 | await gsSession.updateCurrentSession(); 38 | 39 | chrome.runtime.reload(); 40 | // } 41 | }; 42 | } 43 | 44 | function setExportBackupClickHandler() { 45 | document.getElementById('exportBackupBtn').onclick = async function(e) { 46 | const currentSession = await gsSession.buildCurrentSession(); 47 | historyUtils.exportSession(currentSession, function() { 48 | document.getElementById('exportBackupBtn').style.display = 'none'; 49 | setRestartExtensionClickHandler(false); 50 | }); 51 | }; 52 | } 53 | 54 | function setSessionManagerClickHandler() { 55 | document.getElementById('sessionManagerLink').onclick = function(e) { 56 | e.preventDefault(); 57 | chrome.tabs.create({ url: chrome.extension.getURL('history.html') }); 58 | setRestartExtensionClickHandler(false); 59 | }; 60 | } 61 | 62 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 63 | setSessionManagerClickHandler(); 64 | setRestartExtensionClickHandler(true); 65 | setExportBackupClickHandler(); 66 | 67 | var currentVersion = chrome.runtime.getManifest().version; 68 | gsIndexedDb 69 | .fetchSessionRestorePoint(currentVersion) 70 | .then(function(sessionRestorePoint) { 71 | if (!sessionRestorePoint) { 72 | gsUtils.warning( 73 | 'update', 74 | 'Couldnt find session restore point. Something has gone horribly wrong!!' 75 | ); 76 | document.getElementById('noBackupInfo').style.display = 'block'; 77 | document.getElementById('backupInfo').style.display = 'none'; 78 | document.getElementById('exportBackupBtn').style.display = 'none'; 79 | } 80 | }); 81 | }); 82 | })(this); 83 | -------------------------------------------------------------------------------- /src/js/updated.js: -------------------------------------------------------------------------------- 1 | /*global chrome, gsSession, gsUtils */ 2 | (function(global) { 3 | 'use strict'; 4 | 5 | try { 6 | chrome.extension.getBackgroundPage().tgs.setViewGlobals(global); 7 | } catch (e) { 8 | window.setTimeout(() => window.location.reload(), 1000); 9 | return; 10 | } 11 | 12 | function toggleUpdated() { 13 | document.getElementById('updating').style.display = 'none'; 14 | document.getElementById('updated').style.display = 'block'; 15 | } 16 | 17 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() { 18 | var versionEl = document.getElementById('updatedVersion'); 19 | versionEl.innerHTML = 'v' + chrome.runtime.getManifest().version; 20 | 21 | document.getElementById('sessionManagerLink').onclick = function(e) { 22 | e.preventDefault(); 23 | chrome.tabs.create({ url: chrome.extension.getURL('history.html') }); 24 | }; 25 | 26 | var updateType = gsSession.getUpdateType(); 27 | if (updateType === 'major') { 28 | document.getElementById('patchMessage').style.display = 'none'; 29 | document.getElementById('minorUpdateDetail').style.display = 'none'; 30 | } else if (updateType === 'minor') { 31 | document.getElementById('patchMessage').style.display = 'none'; 32 | document.getElementById('majorUpdateDetail').style.display = 'none'; 33 | } else { 34 | document.getElementById('updateDetail').style.display = 'none'; 35 | } 36 | 37 | if (gsSession.isUpdated()) { 38 | toggleUpdated(); 39 | } 40 | }); 41 | 42 | global.exports = { 43 | toggleUpdated, 44 | }; 45 | })(this); 46 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Unofficial The Great Suspender", 3 | "description": "__MSG_ext_extension_description__", 4 | "version": "7.1.5", 5 | "default_locale": "en", 6 | "permissions": [ 7 | "tabs", 8 | "storage", 9 | "history", 10 | "unlimitedStorage", 11 | "http://*/*", 12 | "https://*/*", 13 | "file://*/*", 14 | "contextMenus", 15 | "", 16 | "activeTab", 17 | "cookies" 18 | ], 19 | "background": { 20 | "scripts": ["js/gsUtils.js", "js/gsChrome.js", "js/gsStorage.js", "js/db.js", "js/gsIndexedDb.js", 21 | "js/gsMessages.js", "js/gsSession.js", "js/gsTabQueue.js", "js/gsTabCheckManager.js", "js/gsFavicon.js", 22 | "js/gsTabSuspendManager.js", "js/gsTabDiscardManager.js", "js/gsSuspendedTab.js", "js/background.js"], 23 | "persistent": true 24 | }, 25 | "content_scripts": [ 26 | { 27 | "matches": ["http://*/*", "https://*/*", "file://*/*"], 28 | "js": ["js/contentscript.js"] 29 | } 30 | ], 31 | "browser_action": { 32 | "default_title": "__MSG_ext_default_title__", 33 | "default_icon": { 34 | "16": "img/ic_suspendy_16x16.png", 35 | "32": "img/ic_suspendy_32x32.png" 36 | }, 37 | "default_popup": "popup.html" 38 | }, 39 | "options_ui": { 40 | "page": "options.html", 41 | "open_in_tab": true 42 | }, 43 | "icons": { 44 | "16": "img/ic_suspendy_16x16.png", 45 | "32": "img/ic_suspendy_32x32.png", 46 | "48": "img/ic_suspendy_48x48.png", 47 | "128": "img/ic_suspendy_128x128.png" 48 | }, 49 | "content_security_policy": "script-src 'self'; object-src 'self';", 50 | "incognito": "not_allowed", 51 | "manifest_version": 2, 52 | "minimum_chrome_version": "55", 53 | 54 | "commands": { 55 | "1-suspend-tab": { 56 | "description": "__MSG_ext_cmd_toggle_tab_suspension_description__", 57 | "suggested_key": { "default": "Ctrl+Shift+U" } 58 | }, 59 | "2-toggle-temp-whitelist-tab": { 60 | "description": "__MSG_ext_cmd_toggle_tab_pause_description__" 61 | }, 62 | "2a-suspend-selected-tabs": { 63 | "description": "__MSG_ext_cmd_suspend_selected_tabs_description__" 64 | }, 65 | "2b-unsuspend-selected-tabs": { 66 | "description": "__MSG_ext_cmd_unsuspend_selected_tabs_description__" 67 | }, 68 | "3-suspend-active-window": { 69 | "description": "__MSG_ext_cmd_soft_suspend_active_window_description__" 70 | }, 71 | "3b-force-suspend-active-window": { 72 | "description": "__MSG_ext_cmd_force_suspend_active_window_description__" 73 | }, 74 | "4-unsuspend-active-window": { 75 | "description": "__MSG_ext_cmd_unsuspend_active_window_description__" 76 | }, 77 | "4b-soft-suspend-all-windows": { 78 | "description": "__MSG_ext_cmd_soft_suspend_all_windows_description__" 79 | }, 80 | "5-suspend-all-windows": { 81 | "description": "__MSG_ext_cmd_force_suspend_all_windows_description__" 82 | }, 83 | "6-unsuspend-all-windows": { 84 | "description": "__MSG_ext_cmd_unsuspend_all_windows_description__" 85 | } 86 | }, 87 | "applications": { 88 | "gecko": { 89 | "id": "thegreatsuspender@dvalter" 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/notice.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/permissions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 |

20 | 21 |

22 |

23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 |
15 |
16 | 19 | 22 | 25 | 28 |
29 | 30 |
31 | 34 | 37 |
38 | 39 |
40 | 43 | 46 |
47 | 48 |
49 | 52 |
53 |
54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/recovery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 |
22 |

23 |

24 | 25 |
26 | 27 |

28 |
29 |

30 | 31 |
32 |
33 | 34 | 35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |

43 | 44 |

45 |

46 | 47 | 48 | 49 |

50 |
51 |
52 |
53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/restoring-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 |
17 |

18 |
19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/shortcuts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 20 |
21 |
    22 |
  • 23 | 24 |
  • 25 |
  • 26 | 27 |
  • 28 |
  • 29 | 30 |
  • 31 |
  • 32 | 33 |
  • 34 |
35 |
36 | 37 |
38 | 39 |

40 |
41 | 42 |
43 |
44 | 45 | 46 |
47 |
48 | 49 |
50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/suspended.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ... 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 28 | 29 |
30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | 52 |
53 |
54 |
55 |
56 | The Great Suspender 57 |
58 | 59 |
60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | The Great Suspender is testing 17 | 18 | 19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 |
19 |

20 | 21 | 22 |

23 |
24 |

25 |
26 |
27 |

28 | 29 |

30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 | 38 |
39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/updated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 |

20 |
21 | 22 |
23 |
24 | 25 |
26 |

27 |
28 | 29 |
30 |
31 |
32 |

33 |

34 | 35 |

36 |
37 | 38 |
39 |
40 |

41 | 42 |

43 | 44 |
45 |
    46 |
  • 47 | New option to enable Chrome's built-in memory-saving for suspended tabs.
    48 | Can boost memory savings by up to 5x! 49 |
  • 50 |
  • New right-click context menu options and keyboard shortcuts.
  • 51 |
  • Ability to suspend local file urls.
  • 52 |
  • Improved session saving, export and recovery.
  • 53 |
  • Fixed issues with chrome stealing focus when a tab suspends in the background.
  • 54 |
  • Many other minor bug fixes and performance improvements.
  • 55 |
56 |
57 | 58 |
59 |
60 | 61 |
62 |
    63 |
  • New look UI, themes and icons.
  • 64 |
  • Extension settings can now be synced across computers.
  • 65 |
  • 66 | New option to enable Chrome's built-in memory-saving for suspended tabs.
    67 | Can boost memory savings by up to 5x! 68 |
  • 69 |
  • New right-click context menu options and keyboard shortcuts.
  • 70 |
  • Ability to suspend local file urls.
  • 71 |
  • Improved "Open link in new suspended tab" right-click menu option.
  • 72 |
  • Support for retina favicons and high resolution screen captures.
  • 73 |
  • Suspended youtube tabs now remember current play position.
  • 74 |
  • Session management now supports importing of saved sessions.
  • 75 |
  • Added 1 week & 2 week suspend timer options.
  • 76 |
77 |
78 | 79 |
80 |
    81 |
  • Fixed issue with cookies breaking websites like YouTube.
  • 82 |
  • Fixed issue where suspended tabs showed wrong favicon on restart.
  • 83 |
  • Fixed issues with chrome stealing focus when a tab suspends in the background.
  • 84 |
  • Improved behaviour of automatic tab unsuspending.
  • 85 |
  • Fixed issue with setting the correct scroll location on suspend and unsuspend.
  • 86 |
  • Fixed issue with right-click context menu.
  • 87 |
  • Fixed issue with session exporting.
  • 88 |
  • Many other minor bug fixes and performance improvements.
  • 89 |
90 |
91 |
92 |

93 | github.com/deanoemcke/thegreatsuspender/releases 94 |
95 | Thanks to all the people who have helped with these features and fixes! 96 |

97 |
98 |
99 |
100 | 101 | 102 | 103 | 104 | --------------------------------------------------------------------------------