├── .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 | ![image](https://user-images.githubusercontent.com/1906321/113394317-db3df380-934c-11eb-9629-f31597bcbec6.png) 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 | ![image](https://user-images.githubusercontent.com/1906321/113394341-e5f88880-934c-11eb-864c-66c4a3672e38.png) 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 | ![Config Example](https://i.imgur.com/Vr6P7xp.png) 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 | 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 | 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 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
WinIdTabIdIndexTitleTime to suspendStatus
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 | 37 |
38 | 39 |
40 |

41 |
42 | 43 |

44 |
45 | 46 |

47 |
48 | 49 | 50 | 51 | 52 |

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

23 |

24 | 25 |
26 | 27 |

28 |
29 |

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

43 | 44 |

45 |

46 | 47 | 48 | 49 |

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

18 |
19 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /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 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 51 |
52 |
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 |
28 |
29 |
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 |
19 |

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

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

33 |

34 | 35 |

36 |
37 | 38 |
39 |
40 | 43 | 44 |
45 | 47 |
48 | 49 |
50 | 60 |
61 | 67 |
68 |
69 |
70 | 71 | 72 | 73 | --------------------------------------------------------------------------------