├── .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 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
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 |
Restart extension
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 | WinId
36 | TabId
37 | Index
38 |
39 | Title
40 | Time to suspend
41 | Status
42 |
43 |
44 |
45 |
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 |
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/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 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
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 |
32 |
33 |
34 |
35 |
36 |
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 |
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 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
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 |
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 |
26 |
31 |
32 |
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 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
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 |
--------------------------------------------------------------------------------