├── .eslintrc.json
├── .github
├── issue_template.md
└── stale.yml
├── .gitignore
├── .prettierrc
├── Gruntfile.js
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
└── src
├── _locales
├── de
│ └── messages.json
├── en
│ └── messages.json
├── pt_BR
│ └── messages.json
├── pt_PT
│ └── messages.json
├── ru
│ └── messages.json
├── tr
│ └── 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.js
├── gsChrome.js
├── gsCleanScreencaps.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
└── updated.js
├── managed-storage-schema.json
├── manifest.json
├── notice.html
├── options.html
├── permissions.html
├── popup.html
├── recovery.html
├── restoring-window.html
├── shortcuts.html
├── suspended.html
├── tests.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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Great Suspender - Without Analytics Tracking
2 |
3 | **PLEASE NOTE: If you are switching to this extension from a different version of TheGreatSuspender, first export your tabs from the plugin settings window then, after updating, re-import your suspended tabs. Alternatively unsuspend (or bookmark) your existing suspended tabs before upgrading - you can find "unsuspend all tabs" by clicking on the extension icon in the top right corner of Chrome**
4 |
5 | **Import/Export Instructions: https://i.imgur.com/jgr0qEd.png**
6 |
7 |
8 | Modified version of "The Great Suspender" to remove analytics tracking and rogue .js files from anonymous developer who is now in control of the GitHub source & web store versions.
9 |
10 | Read more:
11 |
12 | [New ownership announcement](https://github.com/greatsuspender/thegreatsuspender/issues/1175)
13 |
14 | [New maintainer is probably malicious](https://github.com/greatsuspender/thegreatsuspender/issues/1263)
15 |
16 | [Flagged as malware by Microsoft Edge](https://www.windowscentral.com/great-suspender-extension-now-flagged-malware-edge-has-built-replacement)
17 |
18 | [Reddit forum discussion](https://old.reddit.com/r/HobbyDrama/comments/jouwq7/open_source_development_the_great_suspender_saga/)
19 |
20 | [Medium Article](https://medium.com/nerd-for-tech/malware-in-browser-extensions-3805e8763dd5)
21 |
22 | This project is a fork from [v7.1.8 of The Great Suspender](https://github.com/greatsuspender/thegreatsuspender) with all tracking code removed, along with some annoying popups/prompts.
23 |
24 | This work carries no guarantees only to the best of my ability in 2 hours using notepad2 & AstroGrep. I am not a developer and do not intend to spend much time keeping this extension updated.
25 |
26 |
27 |
28 | "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.
29 |
30 | If you have suggestions or problems using the extension, please [submit a pull request](https://github.com/aciidic/thegreatsuspender/issues/).
31 |
32 | **If you have lost tabs from your browser:** The original developer has written a guide for how to recover your lost tabs [here](https://github.com/deanoemcke/thegreatsuspender/issues/526
33 | ).
34 |
35 | ### Chrome Web Store
36 |
37 | This version of The Great Suspender is not available on the Chrome Web Store.
38 |
39 |
40 | ### You should install this extension from source
41 |
42 | 1. Download the **[latest available version](https://github.com/aciidic/thegreatsuspender/releases)** and unarchive to your preferred location (whichever suits you).
43 | 2. Using **Google Chrome** browser, navigate to [chrome://extensions/](chrome://extensions/) and enable "Developer mode" in the upper right corner.
44 |
45 | 
46 |
47 | 3. Click on "Load Unpacked" in top-left corner and select the `src` FOLDER from extracted data > click Open Folder
48 | (Or you can try drag & drop the src folder in to your chrome://extensions window)
49 |
50 | 
51 |
52 | 4. Confirm The Great Suspender now appears in chrome://extensions AND in your chrome://policy
53 |
54 | If you have completed the above steps, the "welcome" page will open indicating successful installation of the extension.
55 |
56 | Be sure to unsuspend all suspended tabs before removing any other version of the extension or they will disappear forever!
57 |
58 |
59 | ### Enterprise/Windows Domain installation of extension .crx via Group Policy
60 |
61 | 1. Get extension .crx following steps above or download from [releases](https://github.com/aciidic/thegreatsuspender-notrack/releases)
62 | 2. Install Chrome admx/adml templates [from Google](https://support.google.com/chrome/a/answer/187202?hl=en) on a domain controller
63 | 3. Create new file `Update.xml` on network filestore or similar, and enable read permissions for all relevent domain users/groups
64 | 4. Populate `Update.xml` with code below
65 | ```
66 |
67 |
68 |
69 |
70 |
71 |
72 | ```
73 | 5. Modify `Update.xml` with correct values:
74 | - `app appid=` you can find in chrome://extensions
75 | - `codebase=` leads to extension .crx file. (SMB/network shared folder works fine) *Use forward slash not back slashes!*
76 | - `version=` should be extension version shown in chrome://extensions
77 | 6. Open Group Policy Editor (gpedit.msc) on a domain controller.
78 | 8. Use either `Computer` or `User` policies, locate and enable the policy `Configure the list of force-installed apps and extensions`
79 | - Located at `Policies/Administrative Templates/Google/Google Chrome/Extensions/`
80 | 7. Add the following (UNC path works well) to enforce automatic installation: `App IDs and update URLs to be force installed:`
81 | - `EXTENSION_ID;\\SERVER\SHARE\PATH\TO\Update.xml`
82 | 8. Run `gpupdate.exe` on client machines after adjusting Group Policy enforcement & permissions
83 | 9. **Once installed, if you update the extension & update.xml, but Chrome does not install the new extension version, try disabling the specific Group Policy, run `gpupdate` on clients then re-enable the policy & run `gpupdate` again. This appears to be a Chrome issue.**
84 |
85 |
86 | ### Build from github (untested in this release)
87 |
88 | Dependencies: openssl, npm.
89 |
90 | Clone the repository and run these commands:
91 | ```
92 | npm install
93 | npm run generate-key
94 | npm run build
95 | ```
96 |
97 | It should say:
98 | ```
99 | Done, without errors.
100 | ```
101 |
102 | The extension in crx format will be inside the build/crx/ directory. You can drag it into [extensions] (chrome://extensions) to install locally.
103 |
104 | ### Integrating with another Chrome extension or app
105 |
106 | The old extension had a small external api to allow other extensions to request the suspension of a tab. See [this issue](https://github.com/greatsuspender/thegreatsuspender/issues/276) for more information.
107 |
108 | ### Windows Group Policies / Windows Registry configuration values
109 |
110 | Since extension version 7.1.8 it is possible to set the configuration using the system registy, which can be applied via group policies on Microsoft Windows.
111 | [More Info](https://github.com/greatsuspender/thegreatsuspender/issues/1174)
112 |
113 | The whitelist consists of a list of domains seperated by a space character, *do not include http:// or https://* Here's an example:
114 | `domain1.com www.domain2.com sub.domain3.com`
115 |
116 | Configuration stored in registry can be either HKCU or HKLM at
117 | `\Software\Policies\Google\Chrome\3rdparty\extensions\EXTENSION_ID\policy`
118 |
119 | Replace the EXTENSION_ID with the correct value
120 |
121 | - To enable function `(true)` use REG_DWORD set to 1
122 | - To disable function `(false)` use REG_DWORD set to 0
123 | - When using REG_SZ "quotes" are not required
124 |
125 | *The following settings can be defined:*
126 |
127 | * `SCREEN_CAPTURE` (string, default: '0')
128 | * `SCREEN_CAPTURE_FORCE` (boolean, default: false)
129 | * `SUSPEND_IN_PLACE_OF_DISCARD` (boolean, default: false)
130 | * `DISCARD_IN_PLACE_OF_SUSPEND` (boolean, default: false)
131 | * `USE_ALT_SCREEN_CAPTURE_LIB` (boolean, default: false)
132 | * `DISCARD_AFTER_SUSPEND` (boolean, default: false)
133 | * `IGNORE_WHEN_OFFLINE` (boolean, default: false)
134 | * `IGNORE_WHEN_CHARGING` (boolean, default: false)
135 | * `UNSUSPEND_ON_FOCUS` (boolean, default: false)
136 | * `IGNORE_PINNED` (boolean, default: true)
137 | * `IGNORE_FORMS` (boolean, default: true)
138 | * `IGNORE_AUDIO` (boolean, default: true)
139 | * `IGNORE_ACTIVE_TABS` (boolean, default: true)
140 | * `IGNORE_CACHE` (boolean, default: false)
141 | * `ADD_CONTEXT` (boolean, default: true)
142 | * `SYNC_SETTINGS` (boolean, default: true)
143 | * `ENABLE_CLEAN_SCREENCAPS` (boolean, default: false)
144 | * `SUSPEND_TIME` (string (minutes), default: '60')
145 | * `NO_NAG` (boolean, default: false)
146 | * `WHITELIST` (string (one URL per line), default: '')
147 | * `THEME` (string, default: 'light')
148 |
149 |
150 | **Step by Step:**
151 |
152 | *Note that config changes don't seem to apply until Chrome is restarted, sometimes requires closing/re-opening chrome for a second time*
153 |
154 | 1. Copy the extension ID from chrome://extensions
155 | 2. Create required registry keys (pick either HKLM or HKCU) obviously add your own extension ID, at:
156 | `\Software\Policies\Google\Chrome\3rdparty\extensions\EXTENSION_ID\policy`
157 | - Use REG_SZ for string config values
158 | - Use REG_DWORD for boolean config (1 for true, 0 for false)
159 | - Use REG_SZ for WHITELIST, split each domain with a space char. Extension doesn't care for www. but do not include http/s://
160 | `domain1.com domain2.com www.domain3.com whatever.you.want.com`
161 | 3. **Restart Chrome at least once, if not twice**
162 | 4. Go to chrome://policy and click "Reload policies" in top left, you should see your configuration listed
163 | 
164 |
165 |
166 | ### Contributing to this extension
167 |
168 | 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.
169 |
170 | ### License
171 |
172 | This work is licensed under a GNU GENERAL PUBLIC LICENSE (v2)
173 |
174 | ### Shoutouts
175 |
176 | This package uses the [html2canvas](https://github.com/niklasvh/html2canvas) library written by Niklas von Hertzen.
177 | It also uses the indexedDb wrapper [db.js](https://github.com/aaronpowell/db.js) written by Aaron Powell.
178 | Thank you also to [BrowserStack](https://www.browserstack.com) for providing free chrome testing tools.
179 | Original source from [The Great Suspender v7.1.8](https://github.com/greatsuspender/thegreatsuspender)
180 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "thegreatsuspender-notrack",
3 | "version": "0.0.0",
4 | "description": "A chrome extension for suspending all tabs to free up memory. Without analytics tracking.",
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/aciidic/thegreatsuspender.git"
15 | },
16 | "keywords": [
17 | "chrome",
18 | "extension",
19 | "addon",
20 | "memory",
21 | "suspend",
22 | "tab",
23 | "private",
24 | "privacy",
25 | "notrack"
26 | ],
27 | "author": "aciidic",
28 | "license": "GPLv2",
29 | "bugs": {
30 | "url": "https://github.com/aciidic/thegreatsuspender/issues"
31 | },
32 | "devDependencies": {
33 | "eslint": "^4.19.1",
34 | "eslint-config-prettier": "^2.9.0",
35 | "eslint-config-standard": "^10.2.1",
36 | "eslint-plugin-import": "^2.7.0",
37 | "eslint-plugin-node": "^5.1.1",
38 | "eslint-plugin-promise": "^3.5.0",
39 | "eslint-plugin-standard": "^3.0.1",
40 | "grunt": "~0.4.5",
41 | "grunt-cli": "^1.2.0",
42 | "grunt-contrib-clean": "^1.1.0",
43 | "grunt-contrib-copy": "^1.0.0",
44 | "grunt-crx": "~1.0.5",
45 | "grunt-string-replace": "^1.3.1",
46 | "prettier": "1.13.7",
47 | "time-grunt": "~1.2.1"
48 | },
49 | "dependencies": {
50 | "db.js": "^0.15.0"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | github.com/aciidic/thegreatsuspender
43 |
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 | Original Project by Dean Oemcke
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | left: 0;
93 | padding: 30px 40px 20px;
94 | width: 100%;
95 | text-align: center;
96 | z-index: 101;
97 | }
98 | .faviconWrap {
99 | display: inline-block;
100 | vertical-align: bottom;
101 | margin-bottom: -1px;
102 | margin-right: 10px;
103 | }
104 | .dark .faviconWrapLowContrast {
105 | filter: invert(1) grayscale(1);
106 | }
107 | .gsTopBarImg {
108 | height: 16px;
109 | width: 16px;
110 | }
111 | .gsTopBarTitle {
112 | color: #444;
113 | font-size: 20px;
114 | cursor: default;
115 | }
116 | .gsTopBarUrl {
117 | color: #444;
118 | cursor: pointer;
119 | padding: 0 20px;
120 | }
121 | .gsTopBarUrl,
122 | .gsTopBarTitleWrap {
123 | max-width: 100%;
124 | overflow: hidden;
125 | white-space: nowrap;
126 | text-overflow: ellipsis;
127 | display: inline-block;
128 | margin-bottom: 8px;
129 | }
130 | .hideOverflow {
131 | overflow: hidden;
132 | }
133 | .gsPreviewContainer {
134 | width: 90%;
135 | margin: 0 auto;
136 | padding: 30px 0;
137 | }
138 | .gsPreviewImg {
139 | display: block;
140 | max-width: 100%;
141 | margin: 0 auto;
142 | border-radius: 20px;
143 | overflow: hidden;
144 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
145 | transition: all 0.2s ease;
146 | }
147 | .gsPreviewImg:hover {
148 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
149 | }
150 | .dark .gsPreviewImg {
151 | filter: brightness(70%);
152 | opacity: 0.7;
153 | }
154 | .dark .gsPreviewImg:hover {
155 | opacity: 1;
156 | }
157 | body.img-preview-mode .gsTopBar {
158 | position: relative;
159 | }
160 | .suspended-page {
161 | cursor: pointer;
162 | }
163 | .suspendedMsg {
164 | width: 100vw;
165 | height: 100vh;
166 | line-height: 30px;
167 | display: flex;
168 | align-items: center;
169 | justify-content: center;
170 | flex-direction: column;
171 | text-align: center;
172 | }
173 | .hotkeyCommand {
174 | word-spacing: -1px;
175 | }
176 | .reasonMsg {
177 | font-size: 15px;
178 | margin-bottom: 25px;
179 | }
180 | .suspendedMsg img {
181 | height: 180px;
182 | margin-bottom: 30px;
183 | }
184 | .suspendedMsg-instr {
185 | font-size: 20px;
186 | }
187 | .suspendedMsg-shortcut {
188 | font-size: 15px;
189 | }
190 |
191 | .spinner:before {
192 | content: '';
193 | box-sizing: border-box;
194 | position: fixed;
195 | top: 50%;
196 | left: 50%;
197 | width: 80px;
198 | height: 80px;
199 | margin-top: -40px;
200 | margin-left: -40px;
201 | border-radius: 50%;
202 | border: 8px solid transparent;
203 | border-top-color: #3477db;
204 | animation: spinner 0.6s linear infinite;
205 | z-index: 100;
206 | }
207 | .snoozyWrapper {
208 | position: relative;
209 | }
210 | #snoozySpinner.spinner:before {
211 | border: 2px solid transparent;
212 | border-right-color: #4a4a4a;
213 | border-top-color: #4a4a4a;
214 | animation: spinner 0.6s linear infinite;
215 | margin: 0;
216 | position: absolute;
217 | top: 49px;
218 | right: 7px;
219 | left: auto;
220 | width: 12px;
221 | height: 12px;
222 | }
223 | .suspendedTextWrap {
224 | height: 60px; /* stops slight jump when suspending */
225 | }
226 | .waking .suspendedTextWrap {
227 | opacity: 0;
228 | }
229 | @keyframes spinner {
230 | to {
231 | transform: rotate(360deg);
232 | }
233 | }
234 |
235 | /* dark theme for night lurkers */
236 | .dark,
237 | .dark .gsTopBar,
238 | .dark .suspendedMsg {
239 | background: #222;
240 | }
241 | .dark .suspendedMsg img {
242 | filter: brightness(150%);
243 | }
244 | .dark .gsTopBar,
245 | .dark .gsTopBarTitle,
246 | .dark .gsTopBar a,
247 | .dark .suspendedMsg,
248 | .dark .watermark {
249 | color: #b8b8b8;
250 | }
251 | .dark #setKeyboardShortcut {
252 | text-decoration: underline;
253 | color: #b8b8b8;
254 | }
255 |
256 | /* end suspended tab styles */
257 |
258 | .toast-wrapper {
259 | display: none;
260 | text-align: center;
261 | position: fixed;
262 | z-index: 9999999;
263 | left: 0;
264 | top: 0;
265 | width: 100%;
266 | height: 100%;
267 | opacity: 0;
268 | animation: fadeinout 4s linear forwards;
269 | }
270 | .toast-content {
271 | display: inline-block;
272 | position: relative;
273 | top: 50%;
274 | transform: translateY(-50%);
275 | background-color: #fefefe;
276 | margin: auto;
277 | padding: 10px 20px 20px 20px;
278 | border: 1px solid #888;
279 | border-radius: 5px;
280 | box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
281 | }
282 |
283 | .toast-content p {
284 | font-size: 16px;
285 | }
286 |
287 | @keyframes fadeinout {
288 | 0%,
289 | 100% {
290 | opacity: 0;
291 | visibility: hidden;
292 | }
293 | 5%,
294 | 90% {
295 | opacity: 1;
296 | visibility: visible;
297 | }
298 | }
299 |
300 | @keyframes fadein {
301 | from {
302 | opacity: 0;
303 | }
304 | to {
305 | opacity: 1;
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/src/debug.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | The Great Suspender - Debugger
9 |
10 |
11 |
12 |
13 |
debugErrors:
14 |
|
15 |
debugInfo:
16 |
|
17 |
discardInPlaceOfSuspend:
18 |
|
19 |
useAlternateScreenCaptureLib:
20 |
|
21 |
claim all suspended tabs
22 |
23 |
24 |
25 |
To view debug messages, go to the chrome extensions page.
26 |
27 |
Ensure "developer mode" is checked at the top of the page, then click on the "Inspect views: background page" link for this extension.
28 |
29 |
30 |
31 |
32 |
33 |
34 | WinId
35 | TabId
36 | Index
37 |
38 | Title
39 | Time to suspend
40 | Status
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/font/fontello.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/font/fontello.woff
--------------------------------------------------------------------------------
/src/font/fontello.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/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 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/img/chromeDefaultFavicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/chromeDefaultFavicon.png
--------------------------------------------------------------------------------
/src/img/chromeDefaultFaviconSml.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/chromeDefaultFaviconSml.png
--------------------------------------------------------------------------------
/src/img/chromeDevDefaultFavicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/chromeDevDefaultFavicon.png
--------------------------------------------------------------------------------
/src/img/chromeDevDefaultFaviconSml.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/chromeDevDefaultFaviconSml.png
--------------------------------------------------------------------------------
/src/img/ic_suspendy_128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/ic_suspendy_128x128.png
--------------------------------------------------------------------------------
/src/img/ic_suspendy_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/ic_suspendy_16x16.png
--------------------------------------------------------------------------------
/src/img/ic_suspendy_16x16_grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/ic_suspendy_16x16_grey.png
--------------------------------------------------------------------------------
/src/img/ic_suspendy_32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/ic_suspendy_32x32.png
--------------------------------------------------------------------------------
/src/img/ic_suspendy_32x32_grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/ic_suspendy_32x32_grey.png
--------------------------------------------------------------------------------
/src/img/ic_suspendy_48x48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/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/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/suspendy-guy-alt.png
--------------------------------------------------------------------------------
/src/img/suspendy-guy-oops.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/suspendy-guy-oops.png
--------------------------------------------------------------------------------
/src/img/suspendy-guy-uh-oh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/src/img/suspendy-guy-uh-oh.png
--------------------------------------------------------------------------------
/src/img/suspendy-guy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aciidic/thegreatsuspender-notrack/62a0b5c1be7f35f4a40cde022d4640dad65ff0f7/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 |
27 | })(this);
28 |
--------------------------------------------------------------------------------
/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/aciidic/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 | : gsFavicon.generateChromeFavIconUrlFromUrl(info.tab.url);
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 = `chrome://extensions/?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 | })(this);
148 |
--------------------------------------------------------------------------------
/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/gsCleanScreencaps.js:
--------------------------------------------------------------------------------
1 | var gsCleanScreencaps = {
2 | // this will be filled with domain entries for O(1) lookups during screencaps
3 | blacklist: {},
4 |
5 | // listeners for request coming from a tab that is being suspended
6 | listeners: {},
7 |
8 | // load blacklist on initialization if option is enabled
9 | initAsPromised: async ()=>
10 | {
11 | const useCleanScreencap = gsStorage.getOption(
12 | gsStorage.ENABLE_CLEAN_SCREENCAPS
13 | );
14 |
15 | if (useCleanScreencap) {
16 | await gsCleanScreencaps.loadList()
17 | }
18 |
19 | return;
20 | },
21 |
22 | addListener: (tabId) => {
23 | // remove a listener if there is already one present. That might not be the case, but the function checks for that case.
24 | gsCleanScreencaps.removeListener(tabId);
25 |
26 | const listener = (details) => {
27 | try {
28 | const host = new URL(details.url).host
29 | if (gsCleanScreencaps.blacklist[host]) { return { cancel: true }; }
30 | } catch (err) {
31 | gsUtils.log('background', 'error while trying to block in gsCleanScreencaps', err)
32 | }
33 | }
34 |
35 | chrome.webRequest.onBeforeRequest.addListener(
36 | listener,
37 | { urls: [""], types: ['image'], tabId: tabId },
38 | ["blocking"]
39 | );
40 |
41 | // place a callback that will remove the listener as soon as the suspension
42 | // of the tab succeeded or failed
43 | gsCleanScreencaps.listeners[tabId] = () => chrome.webRequest.onBeforeRequest.removeListener(listener)
44 | },
45 |
46 | // call the remove listener func and remove it from the hashmap
47 | removeListener: (tabId) => {
48 | let tmp;
49 | if (tmp = gsCleanScreencaps.listeners[tabId]) {
50 | delete gsCleanScreencaps[tabId];
51 | tmp();
52 | }
53 | },
54 |
55 | // do nothing but get the data out of the chrome.local.storage
56 | storageData: () => {
57 | return new Promise((res, _) => {
58 | chrome.storage.local.get('gsCleanScreencapsBlacklist', (storage) => res(storage.gsCleanScreencapsBlacklist))
59 | })
60 | },
61 |
62 | loadList: async () => {
63 | const stored = await gsCleanScreencaps.storageData();
64 | // take the blocklist out of storage if it's not existent or newer than 30 days
65 | if (!stored || stored.time + (3600 * 24 * 30) <= new Date().getTime()) {
66 | const rex = /^0.0.0.0 (.*)(?:$|#)/
67 | let resp = await fetch('https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts').then(resp => resp.text())
68 | let m;
69 |
70 | try {
71 | const blockedHosts = resp
72 | .split(/\n/)
73 | .reduce((res, e) => {
74 | if (m = rex.exec(e)) {
75 | res[m[1]] = true;
76 | }
77 | return res;
78 | }, {});
79 |
80 | gsCleanScreencaps.blacklist = blockedHosts;
81 | chrome.storage.local.set({ gsCleanScreencapsBlacklist: { time: new Date().getTime(), blockedHosts } })
82 | return blockedHosts;
83 | } catch (err) {
84 | gsUtils.log('background', 'error while loading blocklist for clean screencapture:', err)
85 | }
86 | } else {
87 | gsCleanScreencaps.blacklist = stored.blockedHosts;
88 | return stored;
89 | }
90 | }
91 | }
--------------------------------------------------------------------------------
/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 | chrome.tabs.executeScript(tabId, { file: scriptPath }, function(response) {
134 | if (chrome.runtime.lastError) {
135 | if (callback) callback(chrome.runtime.lastError);
136 | } else {
137 | if (callback) callback(null, response);
138 | }
139 | });
140 | },
141 |
142 | executeCodeOnTab: function(tabId, codeString, callback) {
143 | if (!tabId) {
144 | if (callback) callback('tabId not specified');
145 | return;
146 | }
147 | chrome.tabs.executeScript(tabId, { code: codeString }, function(response) {
148 | if (chrome.runtime.lastError) {
149 | if (callback) callback(chrome.runtime.lastError);
150 | } else {
151 | if (callback) callback(null, response);
152 | }
153 | });
154 | },
155 | };
156 |
--------------------------------------------------------------------------------
/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 | chrome.tabs.discard(tab.id, () => {
94 | if (chrome.runtime.lastError) {
95 | gsUtils.warning(tab.id, QUEUE_ID, chrome.runtime.lastError);
96 | resolve(false);
97 | } else {
98 | resolve(true);
99 | }
100 | });
101 | }
102 |
103 | function handleDiscardException(
104 | tab,
105 | executionProps,
106 | exceptionType,
107 | resolve,
108 | reject,
109 | requeue
110 | ) {
111 | gsUtils.warning(
112 | tab.id,
113 | QUEUE_ID,
114 | `Failed to discard tab: ${exceptionType}`
115 | );
116 | resolve(false);
117 | }
118 |
119 | async function handleDiscardedUnsuspendedTab(tab) {
120 | if (
121 | gsUtils.shouldSuspendDiscardedTabs() &&
122 | gsTabSuspendManager.checkTabEligibilityForSuspension(tab, 3)
123 | ) {
124 | tgs.setTabStatePropForTabId(tab.id, tgs.STATE_SUSPEND_REASON, 3);
125 | const suspendedUrl = gsUtils.generateSuspendedUrl(tab.url, tab.title, 0);
126 | gsUtils.log(tab.id, QUEUE_ID, 'Suspending discarded unsuspended tab');
127 |
128 | // Note: This bypasses the suspension tab queue and also prevents screenshots from being taken
129 | await gsTabSuspendManager.executeTabSuspension(tab, suspendedUrl);
130 | return;
131 | }
132 | }
133 |
134 | return {
135 | initAsPromised,
136 | queueTabForDiscard,
137 | queueTabForDiscardAsPromise,
138 | unqueueTabForDiscard,
139 | handleDiscardedUnsuspendedTab,
140 | };
141 | })();
142 |
--------------------------------------------------------------------------------
/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 | var migrateTabsEl = document.getElementById('migrateTabs');
247 | migrateTabsEl.onclick = function() {
248 | var migrateTabsFromIdEl = document.getElementById('migrateFromId');
249 | historyUtils.migrateTabs(migrateTabsFromIdEl.value);
250 | };
251 |
252 | //hide incompatible sidebar items if in incognito mode
253 | if (chrome.extension.inIncognitoContext) {
254 | Array.prototype.forEach.call(
255 | document.getElementsByClassName('noIncognito'),
256 | function(el) {
257 | el.style.display = 'none';
258 | }
259 | );
260 | }
261 | }
262 |
263 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() {
264 | render();
265 | });
266 |
267 | })(this);
268 |
--------------------------------------------------------------------------------
/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 | function migrateTabs(from_id) {
189 | if (from_id.length == 32) {
190 | chrome.tabs.query({}, function(tabs){
191 | var count = 0;
192 | var prefix_before = 'chrome-extension://' + from_id;
193 | var prefix_after = 'chrome-extension://' + chrome.i18n.getMessage('@@extension_id');
194 | for (var tab of tabs) {
195 | if (!tab.url.startsWith(prefix_before)) {
196 | continue;
197 | }
198 | count += 1;
199 | var migrated_url = prefix_after + tab.url.substr(prefix_before.length);
200 | chrome.tabs.update(tab.id, {url: migrated_url});
201 | }
202 | alert(chrome.i18n.getMessage('js_history_migrate_success', '' + count));
203 | });
204 | } else {
205 | alert(chrome.i18n.getMessage('js_history_migrate_fail'));
206 | }
207 | }
208 |
209 | return {
210 | importSession,
211 | exportSession,
212 | exportSessionWithId,
213 | validateNewSessionName,
214 | saveSession,
215 | migrateTabs
216 | };
217 | })(this);
218 |
--------------------------------------------------------------------------------
/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 | cleanScreenCaptures: gsStorage.ENABLE_CLEAN_SCREENCAPS,
14 | suspendInPlaceOfDiscard: gsStorage.SUSPEND_IN_PLACE_OF_DISCARD,
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 | noNag: gsStorage.NO_NAG,
27 | timeToSuspend: gsStorage.SUSPEND_TIME,
28 | theme: gsStorage.THEME,
29 | whitelist: gsStorage.WHITELIST,
30 | };
31 |
32 | function selectComboBox(element, key) {
33 | var i, child;
34 |
35 | for (i = 0; i < element.children.length; i += 1) {
36 | child = element.children[i];
37 | if (child.value === key) {
38 | child.selected = 'true';
39 | break;
40 | }
41 | }
42 | }
43 |
44 | // Used to prevent options set in managed storage from being changed
45 | function blockOption(element) {
46 | element.setAttribute('disabled', '');
47 | }
48 |
49 | //populate settings from synced storage
50 | function initSettings() {
51 | var optionEls = document.getElementsByClassName('option'),
52 | pref,
53 | element,
54 | i;
55 |
56 | for (i = 0; i < optionEls.length; i++) {
57 | element = optionEls[i];
58 | pref = elementPrefMap[element.id];
59 | populateOption(element, gsStorage.getOption(pref));
60 | if (gsStorage.isOptionManaged(pref)) {
61 | blockOption(element);
62 | }
63 | }
64 |
65 | setForceScreenCaptureVisibility(
66 | gsStorage.getOption(gsStorage.SCREEN_CAPTURE) !== '0'
67 | );
68 | setCleanScreenCaptureVisibility(
69 | gsStorage.getOption(gsStorage.SCREEN_CAPTURE) !== '0'
70 | );
71 | setAutoSuspendOptionsVisibility(
72 | parseFloat(gsStorage.getOption(gsStorage.SUSPEND_TIME)) > 0
73 | );
74 | setSyncNoteVisibility(!gsStorage.getOption(gsStorage.SYNC_SETTINGS));
75 |
76 | let searchParams = new URL(location.href).searchParams;
77 | if (searchParams.has('firstTime') && !gsStorage.getOption(gsStorage.NO_NAG)) {
78 | document
79 | .querySelector('.welcome-message')
80 | .classList.remove('reallyHidden');
81 | document.querySelector('#options-heading').classList.add('reallyHidden');
82 | }
83 | }
84 |
85 | function populateOption(element, value) {
86 | if (
87 | element.tagName === 'INPUT' &&
88 | element.hasAttribute('type') &&
89 | element.getAttribute('type') === 'checkbox'
90 | ) {
91 | element.checked = value;
92 | } else if (element.tagName === 'SELECT') {
93 | selectComboBox(element, value);
94 | } else if (element.tagName === 'TEXTAREA') {
95 | element.value = value;
96 | }
97 | }
98 |
99 | function getOptionValue(element) {
100 | if (
101 | element.tagName === 'INPUT' &&
102 | element.hasAttribute('type') &&
103 | element.getAttribute('type') === 'checkbox'
104 | ) {
105 | return element.checked;
106 | }
107 | if (element.tagName === 'SELECT') {
108 | return element.children[element.selectedIndex].value;
109 | }
110 | if (element.tagName === 'TEXTAREA') {
111 | return element.value;
112 | }
113 | }
114 |
115 | function setForceScreenCaptureVisibility(visible) {
116 | if (visible) {
117 | document.getElementById('forceScreenCaptureContainer').style.display =
118 | 'block';
119 | } else {
120 | document.getElementById('forceScreenCaptureContainer').style.display =
121 | 'none';
122 | }
123 | }
124 |
125 | function setCleanScreenCaptureVisibility(visible) {
126 | if (visible) {
127 | document.getElementById('cleanScreenCapturesContainer').style.display = 'block';
128 | } else {
129 | document.getElementById('cleanScreenCapturesContainer').style.display = 'none';
130 | }
131 | }
132 |
133 | function setSyncNoteVisibility(visible) {
134 | if (visible) {
135 | document.getElementById('syncNote').style.display = 'block';
136 | } else {
137 | document.getElementById('syncNote').style.display = 'none';
138 | }
139 | }
140 |
141 | function setAutoSuspendOptionsVisibility(visible) {
142 | Array.prototype.forEach.call(
143 | document.getElementsByClassName('autoSuspendOption'),
144 | function(el) {
145 | if (visible) {
146 | el.style.display = 'block';
147 | } else {
148 | el.style.display = 'none';
149 | }
150 | }
151 | );
152 | }
153 |
154 | function handleChange(element) {
155 | return function() {
156 | let prefKey = elementPrefMap[element.id],
157 | interval;
158 | //add specific screen element listeners
159 | switch (prefKey) {
160 | case gsStorage.SCREEN_CAPTURE:
161 | setForceScreenCaptureVisibility(getOptionValue(element) !== '0');
162 | setCleanScreenCaptureVisibility(getOptionValue(element) !== '0');
163 | break;
164 | case gsStorage.SUSPEND_TIME:
165 | interval = getOptionValue(element);
166 | setAutoSuspendOptionsVisibility(interval > 0);
167 | break;
168 | case gsStorage.SYNC_SETTINGS:
169 | if (getOptionValue(element)) {
170 | setSyncNoteVisibility(false);
171 | }
172 | break;
173 | case gsStorage.ENABLE_CLEAN_SCREENCAPS:
174 | if (getOptionValue(element)) {
175 | chrome.runtime.sendMessage({ action: 'loadCleanScreencaptureBlocklist' })
176 | }
177 | break;
178 | }
179 |
180 | var [oldValue, newValue] = saveChange(element);
181 | if (oldValue !== newValue) {
182 | gsUtils.performPostSaveUpdates(
183 | [prefKey],
184 | { [prefKey]: oldValue },
185 | { [prefKey]: newValue }
186 | );
187 | }
188 | };
189 | }
190 |
191 | function saveChange(element) {
192 | var pref = elementPrefMap[element.id],
193 | oldValue = gsStorage.getOption(pref),
194 | newValue = getOptionValue(element);
195 |
196 | //clean up whitelist before saving
197 | if (pref === gsStorage.WHITELIST) {
198 | newValue = gsUtils.cleanupWhitelist(newValue);
199 | }
200 |
201 | //save option
202 | if (oldValue !== newValue) {
203 | gsStorage.setOptionAndSync(elementPrefMap[element.id], newValue);
204 | }
205 |
206 | return [oldValue, newValue];
207 | }
208 |
209 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() {
210 | initSettings();
211 |
212 | var optionEls = document.getElementsByClassName('option'),
213 | element,
214 | i;
215 |
216 | //add change listeners for all 'option' elements
217 | for (i = 0; i < optionEls.length; i++) {
218 | element = optionEls[i];
219 | if (element.tagName === 'TEXTAREA') {
220 | element.addEventListener(
221 | 'input',
222 | gsUtils.debounce(handleChange(element), 200),
223 | false
224 | );
225 | } else {
226 | element.onchange = handleChange(element);
227 | }
228 | }
229 |
230 | document.getElementById('testWhitelistBtn').onclick = async e => {
231 | e.preventDefault();
232 | const tabs = await gsChrome.tabsQuery();
233 | const tabUrls = tabs
234 | .map(
235 | tab =>
236 | gsUtils.isSuspendedTab(tab)
237 | ? gsUtils.getOriginalUrl(tab.url)
238 | : tab.url
239 | )
240 | .filter(
241 | url => !gsUtils.isSuspendedUrl(url) && gsUtils.checkWhiteList(url)
242 | )
243 | .map(url => (url.length > 55 ? url.substr(0, 52) + '...' : url));
244 | if (tabUrls.length === 0) {
245 | alert(chrome.i18n.getMessage('js_options_whitelist_no_matches'));
246 | return;
247 | }
248 | const firstUrls = tabUrls.splice(0, 22);
249 | let alertString = `${chrome.i18n.getMessage(
250 | 'js_options_whitelist_matches_heading'
251 | )}\n${firstUrls.join('\n')}`;
252 |
253 | if (tabUrls.length > 0) {
254 | alertString += `\n${chrome.i18n.getMessage(
255 | 'js_options_whitelist_matches_overflow_prefix'
256 | )} ${tabUrls.length} ${chrome.i18n.getMessage(
257 | 'js_options_whitelist_matches_overflow_suffix'
258 | )}`;
259 | }
260 | alert(alertString);
261 | };
262 |
263 | //hide incompatible sidebar items if in incognito mode
264 | if (chrome.extension.inIncognitoContext) {
265 | Array.prototype.forEach.call(
266 | document.getElementsByClassName('noIncognito'),
267 | function(el) {
268 | el.style.display = 'none';
269 | }
270 | );
271 | window.alert(chrome.i18n.getMessage('js_options_incognito_warning'));
272 | }
273 | });
274 |
275 | global.exports = {
276 | initSettings,
277 | };
278 | })(this);
279 |
--------------------------------------------------------------------------------
/src/js/permissions.js:
--------------------------------------------------------------------------------
1 | /*global chrome, historyUtils, gsSession, 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 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(function() {
13 | document.getElementById('exportBackupBtn').onclick = async function(e) {
14 | const currentSession = await gsSession.buildCurrentSession();
15 | historyUtils.exportSession(currentSession, function() {
16 | document.getElementById('exportBackupBtn').style.display = 'none';
17 | });
18 | };
19 | document.getElementById('setFilePermissiosnBtn').onclick = async function(
20 | e
21 | ) {
22 | await gsChrome.tabsCreate({
23 | url: 'chrome://extensions?id=' + chrome.runtime.id,
24 | });
25 | };
26 | });
27 | })(this);
28 |
--------------------------------------------------------------------------------
/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 = tabToRemove.url || tabToRemove.pendingUrl;
45 | const originalUrl = gsUtils.isSuspendedUrl(url)
46 | ? gsUtils.getOriginalUrl(url)
47 | : url;
48 |
49 | if (
50 | element.getAttribute('data-url') === originalUrl ||
51 | element.getAttribute('data-tabId') == tabToRemove.id
52 | ) {
53 | // eslint-disable-line eqeqeq
54 | recoveryTabsEl.removeChild(element);
55 | }
56 | }
57 |
58 | //if removing the last element.. (re-get the element this function gets called asynchronously
59 | if (document.getElementById('recoveryTabs').children.length === 0) {
60 | //if we have already clicked the restore button then redirect to success page
61 | if (restoreAttempted) {
62 | document.getElementById('suspendy-guy-inprogress').style.display =
63 | 'none';
64 | document.getElementById('recovery-inprogress').style.display = 'none';
65 | document.getElementById('suspendy-guy-complete').style.display =
66 | 'inline-block';
67 | document.getElementById('recovery-complete').style.display =
68 | 'inline-block';
69 |
70 | //otherwise we have no tabs to recover so just hide references to recovery
71 | } else {
72 | hideRecoverySection();
73 | }
74 | }
75 | }
76 |
77 | function showTabSpinners() {
78 | var recoveryTabsEl = document.getElementById('recoveryTabs'),
79 | childLinks = recoveryTabsEl.children;
80 |
81 | for (var i = 0; i < childLinks.length; i++) {
82 | var tabContainerEl = childLinks[i];
83 | tabContainerEl.removeChild(tabContainerEl.firstChild);
84 | var spinnerEl = document.createElement('span');
85 | spinnerEl.classList.add('faviconSpinner');
86 | tabContainerEl.insertBefore(spinnerEl, tabContainerEl.firstChild);
87 | }
88 | }
89 |
90 | function hideRecoverySection() {
91 | var recoverySectionEls = document.getElementsByClassName('recoverySection');
92 | for (var i = 0; i < recoverySectionEls.length; i++) {
93 | recoverySectionEls[i].style.display = 'none';
94 | }
95 | document.getElementById('restoreSession').style.display = 'none';
96 | }
97 |
98 | gsUtils.documentReadyAndLocalisedAsPromsied(document).then(async function() {
99 | var restoreEl = document.getElementById('restoreSession'),
100 | manageEl = document.getElementById('manageManuallyLink'),
101 | previewsEl = document.getElementById('previewsOffBtn'),
102 | recoveryEl = document.getElementById('recoveryTabs'),
103 | warningEl = document.getElementById('screenCaptureNotice'),
104 | tabEl;
105 |
106 | manageEl.onclick = function(e) {
107 | e.preventDefault();
108 | chrome.tabs.create({ url: chrome.extension.getURL('history.html') });
109 | };
110 |
111 | if (previewsEl) {
112 | previewsEl.onclick = function(e) {
113 | gsStorage.setOptionAndSync(gsStorage.SCREEN_CAPTURE, '0');
114 | window.location.reload();
115 | };
116 |
117 | //show warning if screen capturing turned on
118 | if (gsStorage.getOption(gsStorage.SCREEN_CAPTURE) !== '0') {
119 | warningEl.style.display = 'block';
120 | }
121 | }
122 |
123 | var performRestore = async function() {
124 | restoreAttempted = true;
125 | restoreEl.className += ' btnDisabled';
126 | restoreEl.removeEventListener('click', performRestore);
127 | showTabSpinners();
128 | while (gsSession.isInitialising()) {
129 | await gsUtils.setTimeout(200);
130 | }
131 | await gsSession.recoverLostTabs();
132 | };
133 |
134 | restoreEl.addEventListener('click', performRestore);
135 |
136 | const currentTabs = await gsChrome.tabsQuery();
137 | const tabsToRecover = await getRecoverableTabs(currentTabs);
138 | if (tabsToRecover.length === 0) {
139 | hideRecoverySection();
140 | return;
141 | }
142 |
143 | for (var tabToRecover of tabsToRecover) {
144 | tabToRecover.title = gsUtils.getCleanTabTitle(tabToRecover);
145 | tabToRecover.url = gsUtils.getOriginalUrl(tabToRecover.url);
146 | tabEl = await historyItems.createTabHtml(tabToRecover, false);
147 | tabEl.onclick = function() {
148 | return function(e) {
149 | e.preventDefault();
150 | chrome.tabs.create({ url: tabToRecover.url, active: false });
151 | removeTabFromList(tabToRecover);
152 | };
153 | };
154 | recoveryEl.appendChild(tabEl);
155 | }
156 |
157 | var currentSuspendedTabs = currentTabs.filter(o =>
158 | gsUtils.isSuspendedTab(o)
159 | );
160 | for (const suspendedTab of currentSuspendedTabs) {
161 | gsMessages.sendPingToTab(suspendedTab.id, function(error) {
162 | if (error) {
163 | gsUtils.warning(suspendedTab.id, 'Failed to sendPingToTab', error);
164 | } else {
165 | removeTabFromList(suspendedTab);
166 | }
167 | });
168 | }
169 | });
170 |
171 | global.exports = {
172 | removeTabFromList,
173 | };
174 | })(this);
175 |
--------------------------------------------------------------------------------
/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 !== ''
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://google.com';
24 | const isUrl1Valid =
25 | gsUtils.getRootUrl(rawUrl1, false, false) === 'google.com' &&
26 | gsUtils.getRootUrl(rawUrl1, true, false) === 'google.com' &&
27 | gsUtils.getRootUrl(rawUrl1, false, true) === 'https://google.com' &&
28 | gsUtils.getRootUrl(rawUrl1, true, true) === 'https://google.com';
29 |
30 | const rawUrl2 = 'https://google.com/';
31 | const isUrl2Valid =
32 | gsUtils.getRootUrl(rawUrl2, false, false) === 'google.com' &&
33 | gsUtils.getRootUrl(rawUrl2, true, false) === 'google.com' &&
34 | gsUtils.getRootUrl(rawUrl2, false, true) === 'https://google.com' &&
35 | gsUtils.getRootUrl(rawUrl2, true, true) === 'https://google.com';
36 |
37 | const rawUrl3 =
38 | 'https://google.com/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) === 'google.com' &&
41 | gsUtils.getRootUrl(rawUrl3, true, false) === 'google.com/search' &&
42 | gsUtils.getRootUrl(rawUrl3, false, true) === 'https://google.com' &&
43 | gsUtils.getRootUrl(rawUrl3, true, true) ===
44 | 'https://google.com/search';
45 |
46 | const rawUrl4 = 'www.google.com';
47 | const isUrl4Valid =
48 | gsUtils.getRootUrl(rawUrl4, false, false) === 'www.google.com' &&
49 | gsUtils.getRootUrl(rawUrl4, true, false) === 'www.google.com' &&
50 | gsUtils.getRootUrl(rawUrl4, false, true) === 'www.google.com' &&
51 | gsUtils.getRootUrl(rawUrl4, true, true) === 'www.google.com';
52 |
53 | const rawUrl5 =
54 | 'https://github.com/deanoemcke/thegreatsuspender/issues/478#issuecomment-430780678';
55 | const isUrl5Valid =
56 | gsUtils.getRootUrl(rawUrl5, false, false) === 'github.com' &&
57 | gsUtils.getRootUrl(rawUrl5, true, false) ===
58 | 'github.com/deanoemcke/thegreatsuspender/issues/478' &&
59 | gsUtils.getRootUrl(rawUrl5, false, true) === 'https://github.com' &&
60 | gsUtils.getRootUrl(rawUrl5, true, true) ===
61 | 'https://github.com/deanoemcke/thegreatsuspender/issues/478';
62 |
63 | const rawUrl6 = 'file:///Users/dean/Downloads/session%20(63).txt';
64 | const isUrl6Valid =
65 | gsUtils.getRootUrl(rawUrl6, false, false) ===
66 | '/Users/dean/Downloads' &&
67 | gsUtils.getRootUrl(rawUrl6, true, false) ===
68 | '/Users/dean/Downloads/session%20(63).txt' &&
69 | gsUtils.getRootUrl(rawUrl6, false, true) ===
70 | 'file:///Users/dean/Downloads' &&
71 | gsUtils.getRootUrl(rawUrl6, true, true) ===
72 | 'file:///Users/dean/Downloads/session%20(63).txt';
73 |
74 | return assertTrue(
75 | isUrl1Valid &&
76 | isUrl2Valid &&
77 | isUrl3Valid &&
78 | isUrl4Valid &&
79 | isUrl5Valid &&
80 | isUrl6Valid &&
81 | );
82 | },
83 |
84 | // Test gsUtils.executeWithRetries
85 | async () => {
86 | const successPromiseFn = val => new Promise((r, j) => r(val));
87 | let result1;
88 | const timeBefore1 = new Date().getTime();
89 | try {
90 | result1 = await gsUtils.executeWithRetries(
91 | successPromiseFn,
92 | 'a',
93 | 3,
94 | 500
95 | );
96 | } catch (e) {
97 | // do nothing
98 | }
99 | const timeAfter1 = new Date().getTime();
100 | const timeTaken1 = timeAfter1 - timeBefore1;
101 | const isTime1Valid = timeTaken1 >= 0 && timeTaken1 < 100;
102 | const isResult1Valid = result1 === 'a';
103 |
104 | const errorPromiseFn = val => new Promise((r, j) => j());
105 | let result2;
106 | const timeBefore2 = new Date().getTime();
107 | try {
108 | result2 = await gsUtils.executeWithRetries(
109 | errorPromiseFn,
110 | 'b',
111 | 3,
112 | 500
113 | );
114 | } catch (e) {
115 | // do nothing
116 | }
117 | const timeAfter2 = new Date().getTime();
118 | const timeTaken2 = timeAfter2 - timeBefore2;
119 | const isTime2Valid = timeTaken2 >= 1500 && timeTaken2 < 1600;
120 | const isResult2Valid = result2 === undefined;
121 |
122 | return assertTrue(
123 | isResult1Valid && isTime1Valid && isResult2Valid && isTime2Valid
124 | );
125 | },
126 | ];
127 |
128 | return {
129 | name: 'gsUtils Library',
130 | tests,
131 | };
132 | })()
133 | );
134 |
--------------------------------------------------------------------------------
/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/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/managed-storage-schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "object",
3 | "properties": {
4 | "SCREEN_CAPTURE": {
5 | "type": "string"
6 | },
7 | "SCREEN_CAPTURE_FORCE": {
8 | "type": "boolean"
9 | },
10 | "SUSPEND_IN_PLACE_OF_DISCARD": {
11 | "type": "boolean"
12 | },
13 | "DISCARD_IN_PLACE_OF_SUSPEND": {
14 | "type": "boolean"
15 | },
16 | "USE_ALT_SCREEN_CAPTURE_LIB": {
17 | "type": "boolean"
18 | },
19 | "DISCARD_AFTER_SUSPEND": {
20 | "type": "boolean"
21 | },
22 | "IGNORE_WHEN_OFFLINE": {
23 | "type": "boolean"
24 | },
25 | "IGNORE_WHEN_CHARGING": {
26 | "type": "boolean"
27 | },
28 | "UNSUSPEND_ON_FOCUS": {
29 | "type": "boolean"
30 | },
31 | "IGNORE_PINNED": {
32 | "type": "boolean"
33 | },
34 | "IGNORE_FORMS": {
35 | "type": "boolean"
36 | },
37 | "IGNORE_AUDIO": {
38 | "type": "boolean"
39 | },
40 | "IGNORE_ACTIVE_TABS": {
41 | "type": "boolean"
42 | },
43 | "IGNORE_CACHE": {
44 | "type": "boolean"
45 | },
46 | "ADD_CONTEXT": {
47 | "type": "boolean"
48 | },
49 | "SYNC_SETTINGS": {
50 | "type": "boolean"
51 | },
52 | "SUSPEND_TIME": {
53 | "type": "string"
54 | },
55 | "NO_NAG": {
56 | "type": "boolean"
57 | },
58 | "WHITELIST": {
59 | "type": "string"
60 | },
61 | "THEME": {
62 | "type": "string"
63 | }
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "__MSG_ext_extension_name__",
3 | "description": "__MSG_ext_extension_description__",
4 | "version": "7.1.12",
5 | "default_locale": "en",
6 | "permissions": [
7 | "tabs",
8 | "storage",
9 | "history",
10 | "unlimitedStorage",
11 | "http://*/*",
12 | "https://*/*",
13 | "file://*/*",
14 | "chrome://favicon/*",
15 | "contextMenus",
16 | "cookies"
17 | ],
18 | "storage": {
19 | "managed_schema": "managed-storage-schema.json"
20 | },
21 | "background": {
22 | "scripts": ["js/gsUtils.js", "js/gsChrome.js", "js/gsStorage.js", "js/db.js", "js/gsIndexedDb.js",
23 | "js/gsMessages.js", "js/gsSession.js", "js/gsTabQueue.js", "js/gsTabCheckManager.js", "js/gsFavicon.js", "js/gsCleanScreencaps.js",
24 | "js/gsTabSuspendManager.js", "js/gsTabDiscardManager.js", "js/gsSuspendedTab.js", "js/background.js"],
25 | "persistent": true
26 | },
27 | "content_scripts": [
28 | {
29 | "matches": ["http://*/*", "https://*/*", "file://*/*"],
30 | "js": ["js/contentscript.js"]
31 | }
32 | ],
33 | "browser_action": {
34 | "default_title": "__MSG_ext_default_title__",
35 | "default_icon": {
36 | "16": "img/ic_suspendy_16x16.png",
37 | "32": "img/ic_suspendy_32x32.png"
38 | },
39 | "default_popup": "popup.html"
40 | },
41 | "options_page": "options.html",
42 | "icons": {
43 | "16": "img/ic_suspendy_16x16.png",
44 | "32": "img/ic_suspendy_32x32.png",
45 | "48": "img/ic_suspendy_48x48.png",
46 | "128": "img/ic_suspendy_128x128.png"
47 | },
48 | "web_accessible_resources": ["suspended.html"],
49 | "content_security_policy": "script-src 'self'; object-src 'self'; child-src 'self'; connect-src 'self'; img-src 'self' data: chrome:; style-src 'self'; default-src 'self'",
50 | "incognito": "split",
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+S" }
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 | }
88 |
--------------------------------------------------------------------------------
/src/notice.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/popup.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
14 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/shortcuts.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 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/src/suspended.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ...
12 |
13 |
14 |
15 |
16 |
17 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
53 |
54 |
55 | The Great Suspender
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
43 |
44 |
45 |
47 |
48 |
49 |
50 |
60 |
61 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------