├── v2
├── chrome
│ ├── data
│ ├── _locales
│ ├── safe.js
│ ├── background.js
│ └── manifest.json
└── firefox
│ ├── data
│ ├── editor
│ │ ├── vendor
│ │ │ └── README
│ │ ├── trash.svg
│ │ ├── drag.svg
│ │ ├── index.html
│ │ ├── index.js
│ │ └── index.css
│ ├── icons
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 19.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ ├── 38.png
│ │ ├── 48.png
│ │ └── 64.png
│ ├── discard
│ │ ├── icon.png
│ │ ├── index.html
│ │ └── index.js
│ ├── popup
│ │ ├── locked.svg
│ │ ├── unlock.svg
│ │ ├── remove.svg
│ │ ├── overwrite.svg
│ │ ├── preview.svg
│ │ ├── index.html
│ │ ├── index.css
│ │ └── index.js
│ ├── drop
│ │ ├── index.html
│ │ └── index.js
│ └── dialog
│ │ ├── index.js
│ │ └── index.html
│ ├── _locales
│ ├── es
│ │ └── messages.json
│ ├── fr
│ │ └── messages.json
│ ├── pt_BR
│ │ └── messages.json
│ ├── ru
│ │ └── messages.json
│ ├── de
│ │ └── messages.json
│ ├── it
│ │ └── messages.json
│ ├── nl
│ │ └── messages.json
│ └── en
│ │ └── messages.json
│ ├── manifest.json
│ ├── safe.js
│ └── background.js
├── v3
├── data
│ ├── editor
│ │ ├── vendor
│ │ │ └── README
│ │ ├── trash.svg
│ │ ├── drag.svg
│ │ ├── index.html
│ │ ├── index.js
│ │ └── index.css
│ ├── icons
│ │ ├── 128.png
│ │ ├── 16.png
│ │ ├── 19.png
│ │ ├── 256.png
│ │ ├── 32.png
│ │ ├── 38.png
│ │ ├── 48.png
│ │ └── 64.png
│ ├── discard
│ │ ├── icon.png
│ │ ├── index.html
│ │ ├── index.css
│ │ └── index.js
│ ├── popup
│ │ ├── locked.svg
│ │ ├── unlock.svg
│ │ ├── remove.svg
│ │ ├── overwrite.svg
│ │ ├── rename.svg
│ │ ├── preview.svg
│ │ ├── index.html
│ │ ├── index.css
│ │ └── index.js
│ ├── drop
│ │ ├── index.html
│ │ └── index.js
│ └── dialog
│ │ ├── index.html
│ │ ├── index.css
│ │ └── index.js
├── _locales
│ ├── es
│ │ └── messages.json
│ ├── fr
│ │ └── messages.json
│ ├── ru
│ │ └── messages.json
│ ├── pt_BR
│ │ └── messages.json
│ ├── de
│ │ └── messages.json
│ ├── it
│ │ └── messages.json
│ ├── nl
│ │ └── messages.json
│ └── en
│ │ └── messages.json
├── manifest.json
├── safe.js
└── worker.js
├── README.md
├── .github
└── FUNDING.yml
└── LICENSE
/v2/chrome/data:
--------------------------------------------------------------------------------
1 | ../firefox/data
--------------------------------------------------------------------------------
/v2/chrome/_locales:
--------------------------------------------------------------------------------
1 | ../firefox/_locales
--------------------------------------------------------------------------------
/v2/chrome/safe.js:
--------------------------------------------------------------------------------
1 | ../firefox/safe.js
--------------------------------------------------------------------------------
/v2/chrome/background.js:
--------------------------------------------------------------------------------
1 | ../firefox/background.js
--------------------------------------------------------------------------------
/v3/data/editor/vendor/README:
--------------------------------------------------------------------------------
1 | https://github.com/SortableJS/Sortable/archive/1.10.2.zip
2 |
--------------------------------------------------------------------------------
/v2/firefox/data/editor/vendor/README:
--------------------------------------------------------------------------------
1 | https://github.com/SortableJS/Sortable/archive/1.10.2.zip
2 |
--------------------------------------------------------------------------------
/v3/data/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/128.png
--------------------------------------------------------------------------------
/v3/data/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/16.png
--------------------------------------------------------------------------------
/v3/data/icons/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/19.png
--------------------------------------------------------------------------------
/v3/data/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/256.png
--------------------------------------------------------------------------------
/v3/data/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/32.png
--------------------------------------------------------------------------------
/v3/data/icons/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/38.png
--------------------------------------------------------------------------------
/v3/data/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/48.png
--------------------------------------------------------------------------------
/v3/data/icons/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/icons/64.png
--------------------------------------------------------------------------------
/v3/data/discard/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v3/data/discard/icon.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/128.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/16.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/19.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/256.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/32.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/38.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/48.png
--------------------------------------------------------------------------------
/v2/firefox/data/icons/64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/icons/64.png
--------------------------------------------------------------------------------
/v2/firefox/data/discard/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Emano-Waldeck/Save-Tabs/HEAD/v2/firefox/data/discard/icon.png
--------------------------------------------------------------------------------
/v3/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Una extensión del protector de pestañas protegido por contraseña para almacenar pestañas"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Une extension de l'économiseur d'onglets protégé par un mot de passe pour stocker les onglets"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/es/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Una extensión del protector de pestañas protegido por contraseña para almacenar pestañas"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Расширение защищенной паролем заставки вкладок для хранения вкладок в текущем окне или во всех окнах"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/fr/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Une extension de l'économiseur d'onglets protégé par un mot de passe pour stocker les onglets"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Uma extensão de aba protegida por senha para armazenar abas na janela atual, ou em todas as janelas"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/pt_BR/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Uma extensão de aba protegida por senha para armazenar abas na janela atual, ou em todas as janelas"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/ru/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Расширение защищенной паролем заставки вкладок для хранения вкладок в текущем окне или во всех окнах"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Eine passwortgeschützte Tab-Saver-Erweiterung zum Speichern von Tabs im aktuellen Fenster oder in allen Fenstern"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/de/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Eine passwortgeschützte Tab-Saver-Erweiterung zum Speichern von Tabs im aktuellen Fenster oder in allen Fenstern"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un'estensione salvascheda protetta da password per memorizzare le schede nella finestra corrente, o in tutte le finestre"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/nl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Een met een wachtwoord beveiligde tabbladbeveiliging om tabbladen in het huidige venster of in alle vensters op te slaan."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/it/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Un'estensione salvascheda protetta da password per memorizzare le schede nella finestra corrente, o in tutte le finestre"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/nl/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "Een met een wachtwoord beveiligde tabbladbeveiliging om tabbladen in het huidige venster of in alle vensters op te slaan."
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/v3/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "A password-protected tab saver and session manager to store tabs in the current window, or all windows with tab grouping support"
4 | }
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/v2/firefox/_locales/en/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "description": {
3 | "message": "A password-protected tab saver and session manager to store tabs in the current window, or all windows with tab grouping support"
4 | }
5 | }
6 |
7 |
--------------------------------------------------------------------------------
/v3/data/editor/trash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/v2/firefox/data/discard/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/v2/firefox/data/editor/trash.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/v3/data/editor/drag.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/v2/firefox/data/editor/drag.svg:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/v3/data/discard/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/v3/data/popup/locked.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/v3/data/popup/unlock.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/locked.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/unlock.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/v3/data/popup/remove.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/remove.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v3/data/discard/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: light-dark(#fff, #35363a);
3 | --clr: light-dark(#4d5156, #b1b1b1);
4 |
5 | color-scheme: light dark;
6 | }
7 |
8 | html {
9 | height: 100%;
10 | }
11 | body {
12 | font-family: "Helvetica Neue",Helvetica,sans-serif;
13 | font-size: 13px;
14 | color: var(--clr);
15 | background-color: var(--bg);
16 | }
17 |
--------------------------------------------------------------------------------
/v3/data/popup/overwrite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/overwrite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v3/data/popup/rename.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### YouTube Preview
2 | [](https://www.youtube.com/watch?v=9EjeHdj43es)
3 |
4 |
5 | ### Links
6 |
7 | * FAQs Page: https://add0n.com/save-tabs.html
8 | * Chrome Store: https://chrome.google.com/webstore/detail/save-tabs-browser-session/bmpgbfhajjdffipnaonemjdeifamndod
9 | * Edge Add-ons: https://microsoftedge.microsoft.com/addons/detail/save-tabs-browser-sessi/nbcgofangcmpekchfhhmlnopmdijhbjb
10 | * Firefox Add-ons: https://addons.mozilla.org/en-GB/firefox/addon/save-browser-tabs/
11 |
12 |
--------------------------------------------------------------------------------
/v3/data/discard/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const args = new URLSearchParams(location.search);
4 |
5 | if (args.has('href')) {
6 | const href = args.get('href');
7 | document.title = args.get('title') || href;
8 | if (document.hidden) {
9 | document.addEventListener('visibilitychange', () => location.replace(href));
10 | document.querySelector('link[rel="icon"]').href =
11 | `chrome-extension://${chrome.runtime.id}/_favicon/?pageUrl=${encodeURIComponent(href)}&size=32`;
12 | }
13 | else {
14 | location.replace(href);
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/v2/firefox/data/discard/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const args = new URLSearchParams(location.search);
4 |
5 |
6 | if (args.has('href')) {
7 | const href = args.get('href');
8 | document.title = args.get('title') || href;
9 | if (document.hidden) {
10 | document.addEventListener('visibilitychange', () => {
11 | location.replace(href);
12 | });
13 | if (/Firefox/.test(navigator.userAgent) === false) {
14 | document.querySelector('link[rel="icon"]').href = 'chrome://favicon/' + href;
15 | }
16 | }
17 | else {
18 | location.replace(href);
19 | }
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/v2/firefox/data/drop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Drop a JSON
5 |
6 |
28 |
29 |
30 | Drop a Session JSON file into this View
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/v2/chrome/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "version": "0.2.0",
4 | "name": "Save Tabs - Browser Session Manager",
5 | "description": "__MSG_description__",
6 | "default_locale": "en",
7 | "icons": {
8 | "16": "data/icons/16.png",
9 | "19": "data/icons/19.png",
10 | "32": "data/icons/32.png",
11 | "38": "data/icons/38.png",
12 | "48": "data/icons/48.png",
13 | "64": "data/icons/64.png",
14 | "128": "data/icons/128.png",
15 | "256": "data/icons/256.png"
16 | },
17 | "permissions": [
18 | "tabs",
19 | "storage",
20 | "notifications",
21 | "contextMenus",
22 | "chrome://favicon/"
23 | ],
24 | "background": {
25 | "persistent": false,
26 | "scripts": [
27 | "safe.js",
28 | "background.js"
29 | ]
30 | },
31 | "browser_action": {
32 | "default_popup": "data/popup/index.html"
33 | },
34 | "homepage_url": "https://add0n.com/save-tabs.html"
35 | }
36 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: webextension
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/v3/data/drop/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Drop a JSON
5 |
6 |
29 |
30 |
31 | Drop a Session JSON file into this View
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/v3/data/editor/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Manager
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/v2/firefox/data/editor/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Manager
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/v3/data/popup/preview.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v2/firefox/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "version": "0.2.0",
4 | "name": "Save Tabs - Browser Session Manager",
5 | "description": "__MSG_description__",
6 | "default_locale": "en",
7 | "icons": {
8 | "16": "data/icons/16.png",
9 | "19": "data/icons/19.png",
10 | "32": "data/icons/32.png",
11 | "38": "data/icons/38.png",
12 | "48": "data/icons/48.png",
13 | "64": "data/icons/64.png",
14 | "128": "data/icons/128.png",
15 | "256": "data/icons/256.png"
16 | },
17 | "permissions": [
18 | "tabs",
19 | "storage",
20 | "notifications",
21 | "contextMenus",
22 | "cookies",
23 | "contextualIdentities"
24 | ],
25 | "background": {
26 | "persistent": false,
27 | "scripts": [
28 | "safe.js",
29 | "background.js"
30 | ]
31 | },
32 | "browser_action": {
33 | "default_popup": "data/popup/index.html"
34 | },
35 | "sidebar_action": {
36 | "default_panel": "data/popup/index.html?mode=vertical"
37 | },
38 | "homepage_url": "https://add0n.com/save-tabs.html",
39 | "browser_specific_settings": {
40 | "gecko": {
41 | "id": "{08762c8b-d37d-4230-bfdb-a8477be47c34}"
42 | }
43 | },
44 | "commands": {
45 | "_execute_browser_action": {},
46 | "_execute_sidebar_action": {}
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/preview.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/v2/firefox/data/drop/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const args = new URLSearchParams(location.search);
4 |
5 | document.addEventListener('dragover', e => e.preventDefault());
6 |
7 | const read = file => {
8 | if (file.size > 100e6) {
9 | console.warn('more than 100MB session file? I don\'t believe you.');
10 | return;
11 | }
12 | const reader = new FileReader();
13 | reader.onloadend = event => {
14 | const json = JSON.parse(event.target.result);
15 | const next = () => chrome.runtime.reload();
16 | chrome.storage.sync.get(null, prefs => {
17 | if (args.get('command') === 'append') {
18 | const sessions = [...(prefs.sessions || []), ...(json.sessions || [])]
19 | .filter((s, i, l) => s && l.indexOf(s) === i);
20 | Object.assign(prefs, json);
21 | prefs.sessions = sessions;
22 | chrome.storage.sync.set(prefs, next);
23 | }
24 | else if (args.get('command') === 'overwrite') {
25 | chrome.storage.sync.clear(() => chrome.storage.sync.set(json, next));
26 | }
27 | });
28 | };
29 | reader.readAsText(file, 'utf-8');
30 | };
31 |
32 | document.addEventListener('drop', e => {
33 | e.preventDefault();
34 | read(e.dataTransfer.files[0]);
35 | });
36 |
37 | document.querySelector('input').addEventListener('change', e => {
38 | read(e.target.files[0]);
39 | });
40 |
--------------------------------------------------------------------------------
/v3/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "version": "0.3.5",
4 | "name": "Save Tabs - Browser Session Manager",
5 | "description": "__MSG_description__",
6 | "default_locale": "en",
7 | "icons": {
8 | "16": "/data/icons/16.png",
9 | "19": "/data/icons/19.png",
10 | "32": "/data/icons/32.png",
11 | "38": "/data/icons/38.png",
12 | "48": "/data/icons/48.png",
13 | "64": "/data/icons/64.png",
14 | "128": "/data/icons/128.png",
15 | "256": "/data/icons/256.png"
16 | },
17 | "permissions": [
18 | "tabs",
19 | "storage",
20 | "unlimitedStorage",
21 | "notifications",
22 | "contextMenus",
23 | "tabGroups",
24 | "downloads",
25 | "favicon"
26 | ],
27 | "background": {
28 | "service_worker": "worker.js",
29 | "scripts": ["safe.js", "worker.js"]
30 | },
31 | "action": {
32 | "default_popup": "/data/popup/index.html"
33 | },
34 | "homepage_url": "https://add0n.com/save-tabs.html",
35 | "commands": {
36 | "_execute_action": {}
37 | },
38 | "browser_specific_settings": {
39 | "gecko": {
40 | "id": "{08762c8b-d37d-4230-bfdb-a8477be47c34}",
41 | "strict_min_version": "139.0",
42 | "data_collection_permissions": {
43 | "required": ["none"]
44 | },
45 | "optional_permissions": [
46 | "cookies"
47 | ]
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/v3/data/dialog/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Save Tabs
7 |
8 |
9 |
10 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/v2/firefox/safe.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var safe = {};
4 |
5 | {
6 | const toBuffer = str => {
7 | const bytes = new Uint8Array(str.length);
8 | [...str].forEach((c, i) => bytes[i] = c.charCodeAt(0));
9 | return bytes;
10 | };
11 | const toString = buffer => [...buffer].map(b => String.fromCharCode(b)).join('');
12 |
13 | const passwordToKey = password => crypto.subtle.digest({
14 | name: 'SHA-256'
15 | }, toBuffer(password)).then(result => crypto.subtle.importKey('raw', result, {
16 | name: 'AES-CBC'
17 | }, false, ['encrypt', 'decrypt']));
18 |
19 | safe.encrypt = async (data, password) => {
20 | const iv = crypto.getRandomValues(new Uint8Array(16));
21 | const key = await passwordToKey(password);
22 | const result = await crypto.subtle.encrypt({
23 | name: 'AES-CBC',
24 | iv
25 | }, key, toBuffer(data));
26 | return new Promise(resolve => {
27 | const reader = new FileReader();
28 | reader.onload = () => resolve(reader.result);
29 | reader.readAsDataURL(new Blob([iv, result], {type: 'application/octet-binary'}));
30 | });
31 | };
32 | safe.decrypt = async (data, password) => {
33 | const iv = crypto.getRandomValues(new Uint8Array(16));
34 | const key = await passwordToKey(password);
35 | const result = await crypto.subtle.decrypt({
36 | name: 'AES-CBC',
37 | iv
38 | }, key, toBuffer(atob(data.split(',')[1])));
39 | return toString((new Uint8Array(result)).subarray(16));
40 | };
41 | }
42 |
--------------------------------------------------------------------------------
/v2/firefox/data/editor/index.js:
--------------------------------------------------------------------------------
1 | /* global Sortable */
2 | 'use strict';
3 |
4 | const drag = () => {
5 | Sortable.create(document.getElementById('container'), {
6 | animation: 150,
7 | ghostClass: 'blue',
8 | handle: '.handle'
9 | });
10 | };
11 |
12 | document.getElementById('cancel').addEventListener('click', () => {
13 | const iframe = window.top.document.querySelector('iframe');
14 | iframe.dataset.visible = false;
15 | iframe.src = '';
16 | delete iframe.onload;
17 | });
18 |
19 | document.querySelector('form').addEventListener('submit', e => {
20 | e.preventDefault();
21 | const container = document.getElementById('container');
22 | const {session, password} = container;
23 |
24 | const tabs = [...document.querySelectorAll('#container .grid')].map((e, index) => ({
25 | ...e.tab,
26 | title: e.querySelector('input[type=text]:first-of-type').value,
27 | url: e.querySelector('input[type=text]:last-of-type').value,
28 | index
29 | }));
30 | chrome.runtime.sendMessage({
31 | method: 'update',
32 | session,
33 | password,
34 | tabs
35 | }, () => {
36 | document.getElementById('cancel').click();
37 | });
38 | });
39 |
40 | window.build = ({tabs, session, password}) => {
41 | const container = document.getElementById('container');
42 | const t = document.querySelector('template');
43 | for (const tab of tabs) {
44 | const clone = document.importNode(t.content, true);
45 | const [title, url] = [...clone.querySelectorAll('input[type=text]')];
46 | title.value = tab.title;
47 | url.value = tab.url;
48 | clone.querySelector('div').tab = tab;
49 |
50 | container.appendChild(clone);
51 | }
52 | drag();
53 | Object.assign(container, {session, password});
54 | };
55 |
56 | document.getElementById('container').addEventListener('click', e => {
57 | const command = e.target.dataset.command;
58 | if (command === 'remove') {
59 | e.target.closest('.grid').remove();
60 | }
61 | else if (e.detail === 2 && command === 'open') {
62 | chrome.tabs.create({
63 | url: e.target.value
64 | });
65 | }
66 | });
67 |
--------------------------------------------------------------------------------
/v3/data/drop/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const args = new URLSearchParams(location.search);
4 |
5 | document.addEventListener('dragover', e => e.preventDefault());
6 |
7 | const read = file => {
8 | if (file.size > 100e6) {
9 | console.warn('more than 100MB session file? I don\'t believe you.');
10 | return;
11 | }
12 | const reader = new FileReader();
13 | reader.onloadend = async event => {
14 | try {
15 | const json = JSON.parse(event.target.result);
16 |
17 | const lprefs = await chrome.storage.local.get({
18 | sessions: []
19 | });
20 | const sprefs = await chrome.storage.sync.get({
21 | sessions: []
22 | });
23 | if (args.get('command') === 'overwrite') {
24 | await chrome.storage.sync.clear();
25 | sprefs.sessions.length = 0;
26 |
27 | await chrome.storage.local.remove('sessions');
28 | for (const session of lprefs.sessions || []) {
29 | await chrome.storage.local.remove(session);
30 | }
31 | }
32 | for (const session of json.sessions || []) {
33 | try {
34 | await chrome.storage.sync.set({
35 | [session]: json[session]
36 | });
37 | if (sprefs.sessions.includes(session) === false) {
38 | sprefs.sessions.push(session);
39 | }
40 | }
41 | catch (e) {
42 | await chrome.storage.local.set({
43 | [session]: json[session]
44 | });
45 | if (lprefs.sessions.includes(session) === false) {
46 | lprefs.sessions.push(session);
47 | }
48 | }
49 | }
50 | await chrome.storage.sync.set({
51 | sessions: sprefs.sessions
52 | });
53 | await chrome.storage.local.set({
54 | sessions: lprefs.sessions
55 | });
56 | chrome.runtime.reload();
57 | }
58 | catch (e) {
59 | console.error(e);
60 | alert(e.message);
61 | }
62 | };
63 | reader.readAsText(file, 'utf-8');
64 | };
65 |
66 | document.addEventListener('drop', e => {
67 | e.preventDefault();
68 | read(e.dataTransfer.files[0]);
69 | });
70 |
71 | document.querySelector('input').addEventListener('change', e => {
72 | read(e.target.files[0]);
73 | });
74 |
--------------------------------------------------------------------------------
/v3/data/dialog/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: light-dark(#fff, #35363a);
3 | --bg-alt: light-dark(#cbccd2, #4e4e4e);
4 | --clr: light-dark(#4d5156, #b1b1b1);
5 | --dark: light-dark(#d9d9d9, #202124);
6 | --button: light-dark(#f0f0f0, #202124);
7 | --link: light-dark(#1c73ff, #86b5ff);
8 | --border: light-dark(#f1f1f1, #84868e);
9 | --opacity: 0.2;
10 | --invert: 0;
11 |
12 | color-scheme: light dark;
13 | }
14 |
15 | html {
16 | height: 100%;
17 | }
18 | body {
19 | font-family: "Helvetica Neue",Helvetica,sans-serif;
20 | font-size: 13px;
21 | margin: 10px;
22 | height: calc(100% - 20px);
23 | color: var(--clr);
24 | background-color: var(--bg);
25 | }
26 | form {
27 | box-sizing: border-box;
28 | display: grid;
29 | grid-template-columns: min-content 1fr;
30 | grid-template-rows: repeat(6, min-content) 1fr;
31 | white-space: nowrap;
32 | grid-gap: 10px 5px;
33 | height: 100%;
34 | overflow: hidden;
35 | }
36 |
37 | @media screen and (max-width: 400px) {
38 | form {
39 | grid-template-columns: 1fr;
40 | }
41 | form span {
42 | display: none;
43 | }
44 | }
45 |
46 | input[type=text],
47 | input[type=password] {
48 | outline: none;
49 | text-indent: 2px;
50 | border: none;
51 | padding: 5px;
52 | background-color: var(--bg-alt);
53 | }
54 | input[type=submit],
55 | input[type=button] {
56 | border: none;
57 | padding: 5px 20px;
58 | background: var(--button);
59 | cursor: pointer;
60 | }
61 | label {
62 | overflow: hidden;
63 | text-overflow: ellipsis;
64 | }
65 | #tools {
66 | display: flex;
67 | justify-content: flex-end;
68 | align-items: flex-end;
69 | grid-column-start: 1;
70 | grid-column-end: 3;
71 | }
72 | #tools > :last-child {
73 | margin-left: 5px;
74 | }
75 |
76 | @media screen and (max-width: 400px) {
77 | #tools {
78 | display: grid;
79 | grid-template-columns: 1fr 1fr;
80 | grid-column-start: unset;
81 | grid-column-end: unset;
82 | justify-content: unset;
83 | }
84 | }
85 |
86 | input[name="name"] {
87 | margin-bottom: 10px;
88 | }
89 | #internal-container {
90 | display: contents;
91 | }
92 |
93 | @supports (-moz-appearance: none) {
94 | #internal-container * {
95 | opacity: 0.5;
96 | pointer-events: none;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/v2/firefox/data/editor/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-size: 13px;
3 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
4 | margin: 0;
5 | }
6 | form {
7 | height: 100vh;
8 | display: flex;
9 | flex-direction: column;
10 | }
11 | #container {
12 | flex: 1;
13 | }
14 | #tools {
15 | display: grid;
16 | grid-template-columns: min-content min-content;
17 | direction: rtl;
18 | grid-gap: 5px;
19 | padding: 10px;
20 | }
21 | #tools input {
22 | border: none;
23 | background: #f1f1f1;
24 | padding: 10px 20px;
25 | color: #000;
26 | cursor: pointer;
27 | }
28 | .grid {
29 | display: grid;
30 | grid-template-columns: 32px 1fr 1fr 32px;
31 | white-space: nowrap;
32 | border: solid 1px #f1f1f1;
33 | margin-bottom: -1px;
34 | }
35 | @media screen and (max-width: 400px) {
36 | .grid {
37 | grid-template-columns: 32px 1fr 32px;
38 | }
39 | .grid > input {
40 | grid-column-start: 2;
41 | grid-column-end: 3;
42 | }
43 | .grid > span {
44 | grid-row-start: 1;
45 | grid-row-end: 3;
46 | }
47 | .grid > span:first-child {
48 | grid-column-start: 1;
49 | }
50 | .grid > span:last-child {
51 | grid-column-start: 3;
52 | }
53 | }
54 | .grid > span:first-child,
55 | .grid > span:last-child {
56 | background-position: center center;
57 | background-repeat: no-repeat;
58 | opacity: 0.2;
59 | }
60 | .grid > span:first-child {
61 | cursor: ns-resize;
62 | background-image: url('drag.svg');
63 | }
64 | .grid > span:last-child {
65 | cursor: pointer;
66 | background-image: url('trash.svg');
67 | }
68 | .grid:hover > span:first-child,
69 | .grid:hover > span:last-child,
70 | .grid.blue > span:first-child,
71 | .grid.blue > span:last-child {
72 | opacity: 1;
73 | }
74 | .grid.blue {
75 | background-color: #00cdff47;
76 | }
77 | .grid input[type=text] {
78 | border: none;
79 | font-size: inherit;
80 | outline: none;
81 | padding: 10px;
82 | overflow: hidden;
83 | background-color: transparent;
84 | }
85 | @media screen and (max-width: 400px) {
86 | .grid input[type=text] {
87 | padding: 10px 2px;
88 | }
89 | }
90 | .grid input[type=text]:focus {
91 | background-color: #f1f1f1;
92 | }
93 | .grid input[type=text]:nth-child(3) {
94 | color: #0064bd;
95 | cursor: pointer;
96 | }
97 |
--------------------------------------------------------------------------------
/v3/safe.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Safe {
4 | #key = '';
5 |
6 | #encoder = new TextEncoder();
7 | #decoder = new TextDecoder();
8 |
9 | #buffer(string) {
10 | const bytes = new Uint8Array(string.length);
11 | [...string].forEach((c, i) => bytes[i] = c.charCodeAt(0));
12 | return bytes;
13 | }
14 |
15 | async open(password) {
16 | this.#key = await crypto.subtle.digest({
17 | name: 'SHA-256'
18 | }, this.#encoder.encode(password)).then(result => crypto.subtle.importKey('raw', result, {
19 | name: 'AES-CBC'
20 | }, true, ['encrypt', 'decrypt']));
21 | }
22 | export() {
23 | return crypto.subtle.exportKey('raw', this.#key).then(ab => {
24 | return btoa(String.fromCharCode(...new Uint8Array(ab)));
25 | });
26 | }
27 | import(string) {/* Uint8Array */
28 | const decodedKeyData = new Uint8Array(Array.from(atob(string), c => c.charCodeAt(0)));
29 |
30 | return crypto.subtle.importKey('raw', decodedKeyData, {
31 | name: 'AES-CBC'
32 | }, true, ['encrypt', 'decrypt']).then(key => {
33 | this.#key = key;
34 | });
35 | }
36 | async encrypt(string) {
37 | const iv = crypto.getRandomValues(new Uint8Array(16));
38 |
39 | const result = await crypto.subtle.encrypt({
40 | name: 'AES-CBC',
41 | iv
42 | }, this.#key, this.#encoder.encode(string));
43 |
44 | return new Promise(resolve => {
45 | const reader = new FileReader();
46 | reader.onload = () => resolve(reader.result);
47 | reader.readAsDataURL(new Blob([iv, result], {type: 'application/octet-binary'}));
48 | });
49 | }
50 | async decrypt(string) {
51 | // compatibility fix
52 | string = string.replace('data:application/octet-binary;base64,', '');
53 |
54 | const iv = crypto.getRandomValues(new Uint8Array(16));
55 |
56 | const result = await crypto.subtle.decrypt({
57 | name: 'AES-CBC',
58 | iv
59 | }, this.#key, this.#buffer(atob(string)));
60 |
61 | const ab = (new Uint8Array(result)).subarray(16);
62 | return this.#decoder.decode(ab);
63 | }
64 | }
65 |
66 | {
67 | const s = new Safe();
68 | self.safe = {
69 | async encrypt(string, password) {
70 | await s.open(password);
71 | return s.encrypt(string);
72 | },
73 | async decrypt(string, password) {
74 | await s.open(password);
75 | return s.decrypt(string);
76 | }
77 | };
78 | }
79 |
--------------------------------------------------------------------------------
/v3/data/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 | - Save all tabs
15 | - Save and close all tabs
16 | - Save current window tabs
17 | - Save and close current window tabs
18 | - Save other windows tabs
19 | - Save and close other windows tabs
20 | - Save selected tabs
21 |
22 |
31 |
32 |
45 |
46 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/v3/data/editor/index.js:
--------------------------------------------------------------------------------
1 | /* global Sortable */
2 | 'use strict';
3 |
4 | let counter;
5 |
6 | const drag = () => {
7 | Sortable.create(document.getElementById('container'), {
8 | animation: 150,
9 | ghostClass: 'blue',
10 | handle: '.handle'
11 | });
12 | };
13 |
14 | document.getElementById('cancel').addEventListener('click', () => {
15 | const dialog = window.top.document.getElementById('popup');
16 | const iframe = dialog.querySelector('iframe');
17 | iframe.src = '';
18 | delete iframe.onload;
19 | dialog.close();
20 | });
21 |
22 | document.querySelector('form').addEventListener('submit', e => {
23 | e.preventDefault();
24 | const container = document.getElementById('container');
25 | const {session, password} = container;
26 |
27 | const tabs = [...document.querySelectorAll('#container .grid')].map((e, index) => ({
28 | ...e.tab,
29 | title: e.querySelector('input[type=text]:first-of-type').value,
30 | url: e.querySelector('input[type=text]:last-of-type').value,
31 | index
32 | }));
33 | chrome.runtime.sendMessage({
34 | method: 'update',
35 | session,
36 | password,
37 | tabs
38 | }, () => {
39 | counter.textContent = top.counter(tabs.length);
40 | document.getElementById('cancel').click();
41 | });
42 | });
43 |
44 | self.build = ({tabs, session, password, div}) => {
45 | counter = div.querySelector('[data-id="count"]');
46 |
47 | const container = document.getElementById('container');
48 | const t = document.querySelector('template');
49 | for (const tab of tabs) {
50 | const clone = document.importNode(t.content, true);
51 | const [title, url] = [...clone.querySelectorAll('input[type=text]')];
52 | title.value = tab.title;
53 | url.value = tab.url;
54 | clone.querySelector('div').tab = tab;
55 |
56 | container.appendChild(clone);
57 | }
58 | drag();
59 |
60 | Object.assign(container, {
61 | session,
62 | password
63 | });
64 | };
65 |
66 | document.getElementById('container').addEventListener('click', e => {
67 | const command = e.target.dataset.command;
68 | if (command === 'remove') {
69 | e.target.closest('.grid').remove();
70 | }
71 | else if (e.detail === 2 && command === 'open') {
72 | chrome.tabs.create({
73 | url: e.target.value
74 | });
75 | }
76 | });
77 |
78 | document.addEventListener('keydown', e => {
79 | if (e.code === 'Escape') {
80 | e.preventDefault();
81 | e.stopPropagation();
82 | document.getElementById('cancel').click();
83 | }
84 | });
85 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
13 |
14 | - Save all tabs
15 | - Save and close all tabs
16 | - Save current window tabs
17 | - Save and close current window tabs
18 | - Save other windows tabs
19 | - Save and close other windows tabs
20 |
21 |
30 |
31 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/v2/firefox/data/dialog/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const args = new URLSearchParams(location.search);
4 |
5 | const prefs = {
6 | sessions: []
7 | };
8 | chrome.storage.sync.get({
9 | sessions: []
10 | }, ps => Object.assign(prefs.sessions, ps.sessions));
11 |
12 | document.addEventListener('input', ({target}) => {
13 | if (target.type === 'password') {
14 | const password = document.querySelector('[name="password"]');
15 | const confirm = document.querySelector('[name="password-confirm"]');
16 |
17 | confirm.setCustomValidity(
18 | confirm.value === password.value ? '' : 'Does not match with password'
19 | );
20 | }
21 | if (target.name === 'name') {
22 | const bol = prefs.sessions.indexOf('session.' + target.value) === -1;
23 | target.setCustomValidity(bol ? '' : 'This session name is already taken');
24 | }
25 | });
26 |
27 | document.addEventListener('submit', e => {
28 | e.preventDefault();
29 | chrome.runtime.sendMessage({
30 | method: 'store',
31 | name: document.querySelector('[name=name]').value,
32 | password: document.querySelector('[name=password]').value,
33 | rule: args.get('method'),
34 | pinned: document.querySelector('[name=pinned]').checked,
35 | internal: document.querySelector('[name=internal]').checked,
36 | permanent: document.querySelector('[name=permanent]').checked
37 | }, bol => {
38 | window.top.document.querySelector('iframe').dataset.visible = false;
39 | if (bol) {
40 | window.top.close();
41 | top.build();
42 | }
43 | });
44 | });
45 |
46 | document.getElementById('cancel').addEventListener('click', () => {
47 | window.top.document.querySelector('iframe').dataset.visible = false;
48 | });
49 | // Firefox issue
50 | document.addEventListener('DOMContentLoaded', () => {
51 | const name = document.querySelector('[name=name]');
52 | name.value = 'session - ' + Math.random().toString(36).substring(7);
53 | name.focus();
54 | });
55 | // init
56 | {
57 | const internal = document.querySelector('[name=internal]');
58 | const permanent = document.querySelector('[name=permanent]');
59 | const pinned = document.querySelector('[name=pinned]');
60 | chrome.storage.local.get({
61 | 'pinned': true,
62 | 'internal': false,
63 | 'permanent': false
64 | }, prefs => {
65 | pinned.checked = prefs.pinned;
66 | permanent.checked = prefs.permanent;
67 | internal.checked = prefs.internal;
68 | if (args.get('silent') === 'true') {
69 | document.dispatchEvent(new Event('submit'));
70 | }
71 | });
72 | permanent.addEventListener('change', e => chrome.storage.local.set({
73 | 'permanent': e.target.checked
74 | }));
75 | pinned.addEventListener('change', e => chrome.storage.local.set({
76 | 'pinned': e.target.checked
77 | }));
78 | internal.addEventListener('change', e => chrome.storage.local.set({
79 | 'internal': e.target.checked
80 | }));
81 | }
82 |
--------------------------------------------------------------------------------
/v3/data/editor/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: light-dark(#fff, #35363a);
3 | --bg-alt: light-dark(#cbccd2, #4e4e4e);
4 | --clr: light-dark(#4d5156, #b1b1b1);
5 | --dark: light-dark(#d9d9d9, #202124);
6 | --button: light-dark(#f0f0f0, #202124);
7 | --link: light-dark(#1c73ff, #86b5ff);
8 | --border: light-dark(#f1f1f1, #84868e);
9 | --opacity: 0.2;
10 | --invert: 0;
11 |
12 | color-scheme: light dark;
13 | }
14 |
15 | @media (prefers-color-scheme: dark) {
16 | :root {
17 | --opacity: 0.5;
18 | --invert: 1;
19 | }
20 | }
21 |
22 | body {
23 | font-size: 13px;
24 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif;
25 | margin: 0;
26 | color: var(--clr);
27 | background-color: var(--bg);
28 | }
29 | form {
30 | height: 100vh;
31 | display: flex;
32 | flex-direction: column;
33 | }
34 | #container {
35 | flex: 1;
36 | overflow: auto;
37 | }
38 | #tools {
39 | display: grid;
40 | grid-template-columns: min-content min-content;
41 | direction: rtl;
42 | grid-gap: 5px;
43 | padding: 10px;
44 | background-color: var(--bg-alt);
45 | }
46 | #tools input {
47 | border: none;
48 | background: #f1f1f1;
49 | padding: 10px 20px;
50 | color: #000;
51 | cursor: pointer;
52 | }
53 | .grid {
54 | display: grid;
55 | grid-template-columns: 32px 1fr 1fr 32px;
56 | white-space: nowrap;
57 | border: solid 1px var(--border);
58 | margin-bottom: -1px;
59 | }
60 |
61 | @media screen and (max-width: 400px) {
62 | .grid {
63 | grid-template-columns: 32px 1fr 32px;
64 | }
65 | .grid > input {
66 | grid-column-start: 2;
67 | grid-column-end: 3;
68 | }
69 | .grid > span {
70 | grid-row-start: 1;
71 | grid-row-end: 3;
72 | }
73 | .grid > span:first-child {
74 | grid-column-start: 1;
75 | }
76 | .grid > span:last-child {
77 | grid-column-start: 3;
78 | }
79 | }
80 | .grid > span:first-child,
81 | .grid > span:last-child {
82 | background-position: center center;
83 | background-repeat: no-repeat;
84 | opacity: var(--opacity);
85 | }
86 | .grid > span:first-child {
87 | cursor: ns-resize;
88 | background-image: url('drag.svg');
89 | filter: invert(var(--invert));
90 | }
91 | .grid > span:last-child {
92 | cursor: pointer;
93 | background-image: url('trash.svg');
94 | }
95 | .grid:hover > span:first-child,
96 | .grid:hover > span:last-child,
97 | .grid.blue > span:first-child,
98 | .grid.blue > span:last-child {
99 | opacity: 1;
100 | }
101 | .grid.blue {
102 | background-color: #00cdff47;
103 | }
104 | .grid input[type=text] {
105 | border: none;
106 | font-size: inherit;
107 | outline: none;
108 | padding: 10px;
109 | overflow: hidden;
110 | background-color: transparent;
111 | }
112 | @media screen and (max-width: 400px) {
113 | .grid input[type=text] {
114 | padding: 10px 2px;
115 | }
116 | }
117 | .grid input[type=text]:focus {
118 | color: var(--clr) !important;
119 | background-color: var(--bg-alt);
120 | }
121 | .grid input[type=text]:nth-child(3) {
122 | color: var(--link);
123 | cursor: pointer;
124 | }
125 |
--------------------------------------------------------------------------------
/v3/data/dialog/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const args = new URLSearchParams(location.search);
4 |
5 | const prefs = {
6 | sessions: []
7 | };
8 | chrome.storage.sync.get({
9 | sessions: []
10 | }, p1 => {
11 | prefs.sessions.push(p1.sessions);
12 | chrome.storage.local.get({
13 | sessions: []
14 | }, p2 => {
15 | prefs.sessions.push(p2.sessions);
16 | });
17 | });
18 |
19 | document.addEventListener('input', ({target}) => {
20 | if (target.type === 'password') {
21 | const password = document.querySelector('[name="password"]');
22 | const confirm = document.querySelector('[name="password-confirm"]');
23 |
24 | confirm.setCustomValidity(
25 | confirm.value === password.value ? '' : 'Does not match with password'
26 | );
27 | }
28 | if (target.name === 'name') {
29 | const bol = prefs.sessions.indexOf('session.' + target.value) === -1;
30 | target.setCustomValidity(bol ? '' : 'This session name is already taken');
31 | }
32 | });
33 |
34 | document.addEventListener('submit', e => {
35 | e.preventDefault();
36 | chrome.runtime.sendMessage({
37 | method: 'store',
38 | name: document.querySelector('[name=name]').value,
39 | password: document.querySelector('[name=password]').value,
40 | rule: args.get('method'),
41 | pinned: document.querySelector('[name=pinned]').checked,
42 | internal: document.querySelector('[name=internal]').checked,
43 | permanent: document.querySelector('[name=permanent]').checked
44 | }, o => {
45 | if ('error' in o) {
46 | alert(o.error);
47 | }
48 | else {
49 | window.top.document.getElementById('popup').close();
50 | if (o.count > 0) {
51 | window.top.close();
52 | top.build();
53 | }
54 | }
55 | });
56 | });
57 |
58 | document.getElementById('cancel').addEventListener('click', () => {
59 | window.top.document.getElementById('popup').close();
60 | });
61 | // Firefox issue
62 | document.addEventListener('DOMContentLoaded', () => {
63 | const name = document.querySelector('[name=name]');
64 | name.value = 'session - ' + Math.random().toString(36).substring(7);
65 | name.focus();
66 | });
67 | // init
68 | {
69 | const internal = document.querySelector('[name=internal]');
70 | const permanent = document.querySelector('[name=permanent]');
71 | const pinned = document.querySelector('[name=pinned]');
72 | chrome.storage.local.get({
73 | 'pinned': true,
74 | 'internal': false,
75 | 'permanent': true
76 | }, prefs => {
77 | pinned.checked = prefs.pinned;
78 | permanent.checked = prefs.permanent;
79 | internal.checked = prefs.internal;
80 | if (args.get('silent') === 'true') {
81 | document.dispatchEvent(new Event('submit'));
82 | }
83 | });
84 | permanent.addEventListener('change', e => chrome.storage.local.set({
85 | 'permanent': e.target.checked
86 | }));
87 | pinned.addEventListener('change', e => chrome.storage.local.set({
88 | 'pinned': e.target.checked
89 | }));
90 | internal.addEventListener('change', e => chrome.storage.local.set({
91 | 'internal': e.target.checked
92 | }));
93 | }
94 |
95 | document.addEventListener('keydown', e => {
96 | if (e.code === 'Escape') {
97 | e.preventDefault();
98 | e.stopPropagation();
99 | document.getElementById('cancel').click();
100 | }
101 | });
102 |
--------------------------------------------------------------------------------
/v2/firefox/data/dialog/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Save Tabs
6 |
7 |
8 |
9 |
10 |
91 |
112 |
113 |
114 |
115 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/index.css:
--------------------------------------------------------------------------------
1 | @media screen and (max-width: 400px) {
2 | .sh {
3 | display: none;
4 | }
5 | }
6 |
7 | body {
8 | font-family: "Helvetica Neue", Helvetica, sans-serif;
9 | font-size: 13px;
10 | background-color: #fff;
11 | color: #4d5156;
12 | margin: 0;
13 | width: 550px;
14 | padding-bottom: 10px;
15 | user-select: none;
16 | max-height: 500px;
17 | min-height: 240px;
18 | }
19 | body.vertical {
20 | width: unset;
21 | max-height: unset;
22 | min-height: unset;
23 | overflow: hidden;
24 | height: 100vh;
25 | }
26 | @media (pointer: none), (pointer: coarse) {
27 | body {
28 | width: unset;
29 | max-height: unset;
30 | min-height: unset;
31 | overflow: hidden;
32 | }
33 | }
34 | header {
35 | text-indent: 64px;
36 | background: url('../icons/48.png') left 10px center no-repeat;
37 | min-height: 64px;
38 | border-bottom: solid 1px #d4d4d4;
39 | margin-bottom: 10px;
40 | white-space: nowrap;
41 | display: grid;
42 | grid-template-columns: min-content 1fr;
43 | align-items: center;
44 | }
45 | header > span {
46 | font-size: 140%;
47 | }
48 | header > label {
49 | justify-self: end;
50 | margin-right: 10px;
51 | }
52 | h1 {
53 | font-weight: normal;
54 | font-size: 140%;
55 | display: inline-block;
56 | margin: 0;
57 | }
58 | ul {
59 | list-style: none;
60 | margin: 0;
61 | padding: 0;
62 | }
63 | li {
64 | cursor: pointer;
65 | padding: 3px 10px;
66 | white-space: nowrap;
67 | overflow: hidden;
68 | text-overflow: ellipsis;
69 | }
70 | li:hover {
71 | background-color: #eee;
72 | }
73 | table {
74 | width: calc(100% - 10px);
75 | table-layout: fixed;
76 | }
77 | tbody:empty::before {
78 | content: 'no session to restore yet!';
79 | font-style: italic;
80 | color: #ccc;
81 | padding-left: 10px;
82 | }
83 | iframe {
84 | position: absolute;
85 | left: 10px;
86 | width: calc(100% - 20px);
87 | top: 10px;
88 | height: calc(100% - 20px);
89 | border: none;
90 | box-shadow: 0 0 0 100px rgba(0, 0, 0, 0.6);
91 | background-color: #fff;
92 | }
93 | iframe[data-visible=false] {
94 | display: none;
95 | }
96 | input[type=submit],
97 | input[type=button] {
98 | border: none;
99 | padding: 5px 20px;
100 | background: #f1f1f1;
101 | cursor: pointer;
102 | }
103 |
104 | #sessions {
105 | overflow: auto;
106 | display: grid;
107 | grid-template-columns: 1fr 32px min-content 32px min-content 32px 32px;
108 | white-space: nowrap;
109 | grid-column-gap: 3px;
110 | grid-row-gap: 2px;
111 | margin: 5px 10px 5px 8px;
112 | align-items: center;
113 | width: calc(100vw - 18px);
114 | }
115 | #sessions > div {
116 | display: contents;
117 | }
118 |
119 | #sessions td:nth-child(1) {
120 | overflow: hidden;
121 | text-overflow: ellipsis;
122 | white-space: nowrap;
123 | cursor: pointer;
124 | padding: 3px 10px;
125 | }
126 | #sessions [data-session] {
127 | overflow: hidden;
128 | text-overflow: ellipsis;
129 | padding: 2px 5px;
130 | cursor: pointer;
131 | }
132 | #sessions [data-session]:hover {
133 | background-color: #eee;
134 | }
135 | #sessions > div > span:nth-child(2) {
136 | height: 100%;
137 | background-position: center center;
138 | background-repeat: no-repeat;
139 | background-image: url('unlock.svg');
140 | background-size: 16px;
141 | }
142 | #sessions > div[data-locked=true] > span:nth-child(2) {
143 | background-image: url('locked.svg');
144 | }
145 | #sessions [data-cmd=overwrite],
146 | #sessions [data-cmd=preview],
147 | #sessions [data-cmd=remove] {
148 | cursor: pointer;
149 | background-color: #f1f1f1;
150 | background-size: 14px 14px;
151 | background-repeat: no-repeat;
152 | background-position: center center;
153 | height: 18px;
154 | width: 32px;
155 | }
156 | #sessions [data-cmd=overwrite] {
157 | background-image: url(overwrite.svg);
158 | }
159 | #sessions [data-cmd].disabled,
160 | #sessions > div[data-locked=true] [data-cmd=overwrite] {
161 | opacity: 0.2;
162 | pointer-events: none;
163 | }
164 | #sessions [data-cmd=preview] {
165 | background-image: url(preview.svg);
166 | }
167 | #sessions [data-cmd=remove] {
168 | background-image: url(remove.svg);
169 | }
170 | #sessions [data-cmd=overwrite]:hover,
171 | #sessions [data-cmd=preview]:hover,
172 | #sessions [data-cmd=remove]:hover {
173 | background-color: #e45e5e;
174 | color: #fff;
175 | }
176 | #sessions > div[data-permanent=true] [data-session] {
177 | color: #1c73ff;
178 | }
179 | @media screen and (max-width: 400px) {
180 | #sessions > div > span:nth-child(2),
181 | #sessions > div > span:nth-child(3),
182 | #sessions > div > span:nth-child(5) {
183 | display: none;
184 | }
185 | #sessions {
186 | grid-template-columns: 1fr 32px 32px 32px;
187 | grid-column-gap: 5px;
188 | }
189 | }
190 | #prompt {
191 | position: absolute;
192 | left: 0;
193 | top: 0;
194 | width: 100%;
195 | height: 100%;
196 | background-color: rgba(0, 0, 0, 0.2);
197 | display: flex;
198 | align-items: center;
199 | justify-content: center;
200 | }
201 | #prompt[data-visible=false] {
202 | display: none;
203 | }
204 | #prompt > form {
205 | width: calc(100% - 60px);
206 | height: 100px;
207 | background-color: #fff;
208 | border: solid 1px #d4d4d4;
209 | padding: 10px;
210 | }
211 | #prompt input[type=password] {
212 | width: 100%;
213 | box-sizing: border-box;
214 | outline: none;
215 | text-indent: 5px;
216 | padding: 5px;
217 | margin-top: 5px;
218 | }
219 | #prompt[data-type=confirm] input[type=password] {
220 | display: none;
221 | }
222 | #manager {
223 | display: grid;
224 | grid-template-columns: 1fr repeat(3, min-content);
225 | white-space: nowrap;
226 | align-items: center;
227 | margin: 10px;
228 | grid-gap: 5px;
229 | }
230 | #manager > :not(:first-child) {
231 | justify-self: end;
232 | }
233 | @media screen and (max-width: 400px) {
234 | #manager {
235 | grid-template-columns: 1fr min-content min-content;
236 | }
237 | #manager > :first-child {
238 | grid-column-start: 1;
239 | grid-column-end: 4;
240 | }
241 | }
242 |
243 | body[data-count="0"] #manager,
244 | body[data-count="0"] #sessions {
245 | display: none;
246 | }
247 |
248 | .all,
249 | .current,
250 | .other {
251 | padding: 0 2px;
252 | pointer-events: none;
253 | }
254 | .all {
255 | background-color: #ffefbe;
256 | }
257 | .other {
258 | background-color: #c6ffc6;
259 | }
260 | .current {
261 | background-color: #e5e5ff;
262 | }
263 |
264 | [hbox] {
265 | display: flex;
266 | }
267 | [vbox] {
268 | display: flex;
269 | flex-direction: column;
270 | }
271 | [flex="1"] {
272 | flex: 1;
273 | }
274 | [pack=center] {
275 | justify-content: center;
276 | }
277 | [align=center] {
278 | align-items: center;
279 | }
280 | [pack=start] {
281 | justify-content: flex-start;
282 | }
283 | [align=start] {
284 | align-items: flex-start;
285 | }
286 | [pack=end] {
287 | justify-content: flex-end;
288 | }
289 | [align=end] {
290 | align-items: flex-end;
291 | }
292 |
--------------------------------------------------------------------------------
/v2/firefox/data/popup/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (location.href.indexOf('mode=vertical') !== -1) {
4 | document.body.classList.add('vertical');
5 | }
6 |
7 | const prompt = document.getElementById('prompt');
8 | const ask = (msg, type = 'prompt') => new Promise((resolve, reject) => {
9 | const password = prompt.querySelector('[type=password]');
10 | const callback = e => {
11 | e.preventDefault();
12 | prompt.reject = '';
13 | prompt.removeEventListener('submit', callback);
14 | prompt.dataset.visible = false;
15 | resolve(password.value);
16 | };
17 | prompt.querySelector('span').textContent = msg;
18 | prompt.dataset.visible = true;
19 | prompt.dataset.type = type;
20 | password[type === 'prompt' ? 'setAttribute' : 'removeAttribute']('required', true);
21 | prompt.addEventListener('submit', callback);
22 | prompt.reject = reject;
23 | if (type === 'prompt') {
24 | password.focus();
25 | password.value = '';
26 | }
27 | });
28 | prompt.addEventListener('click', ({target}) => {
29 | if (target === prompt) {
30 | prompt.dataset.visible = false;
31 | if (prompt.reject) {
32 | prompt.reject();
33 | }
34 | }
35 | });
36 | prompt.querySelector('input[type=button]').addEventListener('click', () => {
37 | prompt.dispatchEvent(new Event('click'));
38 | });
39 |
40 | document.addEventListener('click', async e => {
41 | const target = e.target;
42 | const method = target.dataset.cmd;
43 | if (method === 'remove') {
44 | await ask('The session data will be erased. Are you sure?', 'confirm');
45 | chrome.storage.sync.get({
46 | sessions: []
47 | }, prefs => {
48 | const div = target.closest('div[data-session]');
49 | const {session} = div.dataset;
50 | const index = prefs.sessions.indexOf(session);
51 | prefs.sessions.splice(index, 1);
52 | document.body.dataset.count = prefs.sessions.length;
53 | chrome.storage.sync.set(prefs, () => chrome.storage.sync.remove(session, () => {
54 | div.remove();
55 | }));
56 | });
57 | }
58 | else if (method === 'preview') {
59 | const div = target.closest('div[data-session]');
60 | const {locked, session} = div.dataset;
61 | const password = locked === 'true' ? await ask('Enter the Session Password') : '';
62 |
63 | const iframe = document.querySelector('iframe');
64 | iframe.onload = () => chrome.runtime.sendMessage({
65 | method,
66 | session,
67 | password
68 | }, tabs => {
69 | if (Array.isArray(tabs)) {
70 | iframe.contentWindow.build({tabs, password, session});
71 | }
72 | else {
73 | iframe.dataset.visible = false;
74 | delete iframe.onload;
75 | }
76 | });
77 | iframe.dataset.visible = true;
78 | iframe.src = '/data/editor/index.html';
79 | }
80 | else if (method === 'restore') {
81 | const div = target.closest('div[data-session]');
82 | const {locked, session, permanent} = div.dataset;
83 | chrome.runtime.sendMessage({
84 | method,
85 | session,
86 | password: locked === 'true' ? await ask('Enter the Session Password') : '',
87 | remove: e.shiftKey === false,
88 | single: document.getElementById('single').checked,
89 | discard: document.getElementById('discard').checked,
90 | clean: document.getElementById('clean').checked
91 | }, () => {
92 | if (permanent !== 'true') {
93 | window.close();
94 | div.remove();
95 | }
96 | });
97 | }
98 | else if (method && method.startsWith('save-')) {
99 | const iframe = document.querySelector('iframe');
100 | iframe.dataset.visible = true;
101 | iframe.src = '/data/dialog/index.html?method=' + method + '&silent=' + document.getElementById('silent').checked;
102 | }
103 | else if (method === 'overwrite') {
104 | await ask('Your session will be overwritten by open tabs. Are you sure?', 'confirm');
105 | e.target.classList.add('disabled');
106 | const div = target.closest('div[data-session]');
107 | const {session} = div.dataset;
108 | chrome.storage.local.get({
109 | 'pinned': true,
110 | 'internal': false
111 | }, prefs => chrome.runtime.sendMessage({
112 | method: 'overwrite',
113 | session,
114 | rule: 'save-tabs',
115 | ...prefs
116 | }, length => {
117 | e.target.classList.remove('disabled');
118 | if (length) {
119 | e.target.closest('[data-session]').querySelector('[data-id=count]').textContent = length + ' Tabs';
120 | }
121 | }));
122 | }
123 | else if (method) {
124 | chrome.runtime.sendMessage({
125 | method
126 | });
127 | }
128 | });
129 |
130 | const format = num => {
131 | const d = new Date(num);
132 | return `${d.getFullYear().toString().substr(-2)}.${('00' + (d.getMonth() + 1)).substr(-2)}.${('00' + d.getDate()).substr(-2)} ` +
133 | `${('00' + d.getHours()).substr(-2)}:${('00' + d.getMinutes()).substr(-2)}`;
134 | };
135 |
136 | const build = () => chrome.storage.sync.get(null, prefs => {
137 | const sessions = document.getElementById('sessions');
138 | sessions.textContent = '';
139 | const f = document.createDocumentFragment();
140 |
141 | prefs.sessions = prefs.sessions || [];
142 | document.body.dataset.count = prefs.sessions.length;
143 | prefs.sessions.forEach(session => {
144 | const obj = prefs[session] || {};
145 | // fix password protected
146 | obj.protected = obj.json.startsWith('data:application/octet-binary;');
147 |
148 | const div = document.createElement('div');
149 | div.dataset.session = session;
150 | div.dataset.locked = obj.protected;
151 | const name = document.createElement('span');
152 | name.textContent = session.replace(/^session\./, '');
153 | name.dataset.session = session;
154 | name.dataset.cmd = 'restore';
155 | name.title = name.textContent + `
156 |
157 | Shift + click: restore without removing the session`;
158 | div.appendChild(name);
159 | div.appendChild(document.createElement('span'));
160 | const number = document.createElement('span');
161 | number.dataset.id = 'count';
162 | number.textContent = obj.tabs + ' Tabs';
163 | div.appendChild(number);
164 | const preview = document.createElement('span');
165 | preview.dataset.cmd = 'preview';
166 | preview.title = 'Preview this Session';
167 | div.appendChild(preview);
168 | const date = document.createElement('span');
169 | date.textContent = format(obj.timestamp);
170 | div.appendChild(date);
171 | const overwrite = document.createElement('span');
172 | overwrite.dataset.cmd = 'overwrite';
173 | overwrite.title = 'Overwrite this Session with Open Tabs';
174 | div.appendChild(overwrite);
175 | const close = document.createElement('span');
176 | close.dataset.cmd = 'remove';
177 | close.title = 'Remove this Session';
178 | div.appendChild(close);
179 | div.dataset.permanent = obj.permanent;
180 | f.appendChild(div);
181 | });
182 | sessions.appendChild(f);
183 | });
184 | window.build = build;
185 | document.addEventListener('DOMContentLoaded', build);
186 |
187 | // persist
188 | document.addEventListener('DOMContentLoaded', () => chrome.storage.local.get({
189 | 'silent': false,
190 | 'single': false,
191 | 'discard': false,
192 | 'clean': false
193 | }, prefs => {
194 | document.getElementById('silent').checked = prefs.silent;
195 | document.getElementById('single').checked = prefs.single;
196 | document.getElementById('normal').checked = prefs.single === false;
197 | document.getElementById('discard').checked = prefs.discard;
198 | document.getElementById('clean').checked = prefs.clean;
199 | }));
200 | document.getElementById('silent').addEventListener('change', e => chrome.storage.local.set({
201 | 'silent': e.target.checked
202 | }));
203 | document.getElementById('manager').addEventListener('change', () => chrome.storage.local.set({
204 | 'single': document.getElementById('single').checked,
205 | 'discard': document.getElementById('discard').checked,
206 | 'clean': document.getElementById('clean').checked
207 | }));
208 |
--------------------------------------------------------------------------------
/v3/data/popup/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg: light-dark(#fff, #35363a);
3 | --clr: light-dark(#4d5156, #b1b1b1);
4 | --dark: light-dark(#d9d9d9, #202124);
5 | --button: light-dark(#f1f1f1, #b7b7b7);
6 | --button-fg: light-dark(#000, #000);
7 | --bg-hover: light-dark(#f3f3f3, #4a4a4a);
8 | --bg-hover-alt: light-dark(#f3f3f3, #a2a2a2);
9 | --clr-hover: light-dark(#000, #eee);
10 | --link: light-dark(#1c73ff, #86b5ff);
11 | --invert: 0;
12 | --brightness: 1;
13 |
14 | color-scheme: light dark;
15 | }
16 |
17 | @media (prefers-color-scheme: dark) {
18 | :root {
19 | --invert: 1;
20 | --brightness: 2;
21 | }
22 | }
23 |
24 | @media screen and (max-width: 400px) {
25 | .sh {
26 | display: none;
27 | }
28 | }
29 |
30 | body {
31 | font-family: "Helvetica Neue", Helvetica, sans-serif;
32 | font-size: 13px;
33 | background-color: var(--bg);
34 | color: var(--clr);
35 | margin: 0;
36 | width: 600px;
37 | padding-bottom: 10px;
38 | user-select: none;
39 | max-height: 500px;
40 | min-height: 240px;
41 | }
42 | body:has(dialog[open]) {
43 | min-height: 300px;
44 | }
45 | body.vertical {
46 | width: unset;
47 | max-height: unset;
48 | min-height: unset;
49 | overflow: hidden;
50 | height: 100vh;
51 | }
52 | @media (pointer: none), (pointer: coarse) {
53 | body {
54 | width: unset;
55 | max-height: unset;
56 | min-height: unset;
57 | overflow: hidden;
58 | }
59 | }
60 | header {
61 | text-indent: 64px;
62 | background: url('../icons/128.png') left 10px center no-repeat;
63 | background-size: 48px;
64 | min-height: 64px;
65 | border-bottom: solid 3px #d4d4d4;
66 | margin-bottom: 10px;
67 | white-space: nowrap;
68 | display: grid;
69 | grid-template-columns: 1fr min-content ;
70 | align-items: center;
71 | }
72 | header > span {
73 | font-size: 140%;
74 | }
75 | header > label {
76 | margin-right: 10px;
77 | white-space: nowrap;
78 | display: flex;
79 | gap: 5px;
80 | text-indent: initial;
81 | }
82 | h1 {
83 | font-weight: normal;
84 | font-size: 140%;
85 | display: inline-block;
86 | margin: 0;
87 | }
88 | ul {
89 | list-style: none;
90 | margin: 0;
91 | padding: 0;
92 | }
93 | li {
94 | cursor: pointer;
95 | padding: 3px 10px;
96 | white-space: nowrap;
97 | overflow: hidden;
98 | text-overflow: ellipsis;
99 | }
100 | li:hover {
101 | background-color: var(--bg-hover);
102 | color: var(--clr-hover);
103 | }
104 | table {
105 | width: calc(100% - 10px);
106 | table-layout: fixed;
107 | }
108 | tbody:empty::before {
109 | content: 'no session to restore yet!';
110 | font-style: italic;
111 | color: #ccc;
112 | padding-left: 10px;
113 | }
114 | #popup {
115 | padding: 0;
116 | border: none;
117 | }
118 | #popup[open] {
119 | display: flex;
120 | }
121 | #popup iframe {
122 | width: calc(100vw - 60px);
123 | height: calc(100vh - 60px);
124 | border: none;
125 | background-color: var(--bg);
126 | }
127 | input[type=submit],
128 | input[type=button] {
129 | border: none;
130 | padding: 5px 20px;
131 | color: var(--button-fg);
132 | background: var(--button);
133 | cursor: pointer;
134 | }
135 | input[type=checkbox],
136 | input[type=radio] {
137 | margin: 0 5px;
138 | }
139 |
140 | #sessions {
141 | overflow: auto;
142 | display: grid;
143 | grid-template-columns: 1fr min-content min-content min-content min-content min-content min-content min-content min-content min-content;
144 | white-space: nowrap;
145 | grid-gap: 5px 2px;
146 | margin: 5px 10px 5px 8px;
147 | align-items: center;
148 | width: calc(100vw - 18px);
149 | }
150 | #sessions > div {
151 | display: contents;
152 | }
153 | #sessions > div.header > * {
154 | padding: 3px;
155 | position: sticky;
156 | top: 0;
157 | background-color: var(--bg-hover);
158 | text-align: center;
159 | font-weight: 500;
160 | z-index: 2;
161 | }
162 |
163 | #sessions td:nth-child(1) {
164 | overflow: hidden;
165 | text-overflow: ellipsis;
166 | white-space: nowrap;
167 | cursor: pointer;
168 | padding: 3px 10px;
169 | }
170 | #sessions [data-session] {
171 | overflow: hidden;
172 | text-overflow: ellipsis;
173 | cursor: pointer;
174 | }
175 | #sessions [data-session]:hover {
176 | background-color: var(--bg-hover);
177 | }
178 | #sessions [data-id="lock"] {
179 | height: 100%;
180 | background-position: center center;
181 | background-repeat: no-repeat;
182 | background-image: url('unlock.svg');
183 | filter: brightness(var(--brightness));
184 | background-size: 16px;
185 | }
186 | #sessions > div[data-locked="true"] > [data-id="lock"] {
187 | background-image: url('locked.svg');
188 | filter: none;
189 | }
190 | #sessions [data-id="permanent"],
191 | #sessions [data-id="synced"] {
192 | text-align: center;
193 | }
194 | #sessions [data-cmd=overwrite],
195 | #sessions [data-cmd=preview],
196 | #sessions [data-cmd=rename],
197 | #sessions [data-cmd=remove] {
198 | cursor: pointer;
199 | background-color: var(--button);
200 | filter: invert(var(--invert));
201 | background-size: 14px 14px;
202 | background-repeat: no-repeat;
203 | background-position: center center;
204 | height: 18px;
205 | width: 32px;
206 | }
207 | #sessions [data-cmd=overwrite] {
208 | background-image: url("overwrite.svg");
209 | }
210 | #sessions [data-cmd].disabled,
211 | #sessions > div[data-locked=true] [data-cmd=overwrite] {
212 | opacity: 0.2;
213 | pointer-events: none;
214 | }
215 | #sessions [data-cmd=preview] {
216 | background-image: url("preview.svg");
217 | }
218 | #sessions [data-cmd=rename] {
219 | background-image: url("rename.svg");
220 | }
221 | #sessions [data-cmd=remove] {
222 | background-image: url("remove.svg");
223 | }
224 | #sessions [data-cmd=overwrite]:hover,
225 | #sessions [data-cmd=rename]:hover,
226 | #sessions [data-cmd=preview]:hover,
227 | #sessions [data-cmd=remove]:hover {
228 | background-color: var(--bg-hover-alt);
229 | color: var(--clr-hover);
230 | }
231 | #sessions > div [data-session] {
232 | color: var(--link);
233 | padding: 3px;
234 | }
235 | #prompt {
236 | padding: 0;
237 | border: none;
238 | }
239 | #prompt[open] {
240 | display: flex;
241 | }
242 | #prompt > form {
243 | width: calc(100vw - 60px);
244 | height: 100px;
245 | background-color: var(--bg);
246 | border: solid 1px #d4d4d4;
247 | padding: 10px;
248 | }
249 | #prompt input[type="text"],
250 | #prompt input[type="password"] {
251 | width: 100%;
252 | box-sizing: border-box;
253 | outline: none;
254 | text-indent: 5px;
255 | padding: 5px;
256 | margin-top: 5px;
257 | }
258 | #prompt:not([data-type="password"]) input[type="password"] {
259 | display: none;
260 | }
261 | #prompt:not([data-type="prompt"]) input[type="text"] {
262 | display: none;
263 | }
264 | #manager {
265 | display: grid;
266 | grid-template-columns: 1fr repeat(3, min-content);
267 | white-space: nowrap;
268 | align-items: center;
269 | margin: 10px;
270 | grid-gap: 5px;
271 | }
272 | #manager > :not(:first-child) {
273 | justify-self: end;
274 | }
275 |
276 | #manager div,
277 | #manager label {
278 | display: flex;
279 | align-items: center;
280 | }
281 |
282 | @media screen and (max-width: 400px) {
283 | #manager {
284 | grid-template-columns: 1fr min-content min-content;
285 | }
286 | #manager > :first-child {
287 | grid-column-start: 1;
288 | grid-column-end: 4;
289 | }
290 | }
291 |
292 | body[data-count="0"] #manager,
293 | body[data-count="0"] #sessions {
294 | display: none;
295 | }
296 |
297 | .all,
298 | .current,
299 | .selected,
300 | .other {
301 | padding: 0 2px;
302 | pointer-events: none;
303 | }
304 | .all {
305 | color: #000;
306 | background-color: #ffefbe;
307 | }
308 | .other {
309 | color: #000;
310 | background-color: #c6ffc6;
311 | }
312 | .current {
313 | color: #000;
314 | background-color: #e5e5ff;
315 | }
316 | .selected {
317 | color: #000;
318 | background-color: #ffd6ed;
319 | }
320 |
321 | [hbox] {
322 | display: flex;
323 | }
324 | [vbox] {
325 | display: flex;
326 | flex-direction: column;
327 | }
328 | [flex="1"] {
329 | flex: 1;
330 | }
331 | [pack=center] {
332 | justify-content: center;
333 | }
334 | [align=center] {
335 | align-items: center;
336 | }
337 | [pack=start] {
338 | justify-content: flex-start;
339 | }
340 | [align=start] {
341 | align-items: flex-start;
342 | }
343 | [pack=end] {
344 | justify-content: flex-end;
345 | }
346 | [align=end] {
347 | align-items: flex-end;
348 | }
349 |
--------------------------------------------------------------------------------
/v2/firefox/background.js:
--------------------------------------------------------------------------------
1 | /* globals safe */
2 | 'use strict';
3 |
4 | const storage = {
5 | get: prefs => new Promise(resolve => chrome.storage.sync.get(prefs, resolve)),
6 | set: prefs => new Promise(resolve => chrome.storage.sync.set(prefs, resolve)),
7 | remove: arr => new Promise(resolve => chrome.storage.sync.remove(arr, resolve))
8 | };
9 |
10 | const notify = message => chrome.notifications.create({
11 | type: 'basic',
12 | title: chrome.runtime.getManifest().name,
13 | message,
14 | iconUrl: 'data/icons/48.png'
15 | });
16 |
17 | chrome.runtime.onMessage.addListener((request, sender, response) => {
18 | if (request.method === 'store' || request.method === 'overwrite') {
19 | recording.perform(request, response, request.method === 'store' ? 'new' : 'update');
20 | return true;
21 | }
22 | else if (request.method === 'update') {
23 | recording.disk(request.tabs, request, 'update');
24 | response(true);
25 | }
26 | else if (request.method === 'restore' || request.method === 'preview') {
27 | storage.get({
28 | [request.session]: {},
29 | 'sessions': []
30 | }).then(async prefs => {
31 | const session = prefs[request.session];
32 | session.protected = session.json.startsWith('data:application/octet-binary;');
33 | try {
34 | const tabs = JSON.parse(
35 | session.protected ? await safe.decrypt(session.json, request.password) : session.json
36 | );
37 | if (request.method === 'preview') {
38 | return response(tabs);
39 | }
40 | // remove currents
41 | const removeTabs = [];
42 | if (request.clean) {
43 | await new Promise(resolve => chrome.tabs.query({}, tabs => {
44 | removeTabs.push(...tabs);
45 | resolve();
46 | }));
47 | }
48 | // restore
49 | const create = (tab, props) => new Promise(resolve => {
50 | const discarded = request.discard && tab.active !== true;
51 | if (/Firefox/.test(navigator.userAgent)) {
52 | props = {...props, discarded, url: tab.url};
53 | if (discarded) {
54 | props.title = tab.title;
55 | }
56 | chrome.tabs.create(props, resolve);
57 | }
58 | else {
59 | let url = tab.url;
60 | if (discarded && url.startsWith('http')) {
61 | url = chrome.runtime.getURL('data/discard/index.html?href=' +
62 | encodeURIComponent(tab.url)) + '&title=' + encodeURIComponent(tab.title);
63 | }
64 | chrome.tabs.create({
65 | ...props,
66 | url
67 | }, resolve);
68 | }
69 | });
70 | const groups = {};
71 | if (request.single) {
72 | for (const t of tabs) {
73 | const props = {
74 | pinned: t.pinned,
75 | active: t.active
76 | };
77 | if ('cookieStoreId' in t) {
78 | props.cookieStoreId = t.cookieStoreId;
79 | }
80 | const tab = await create(t, props);
81 | if ('groupId' in t) {
82 | groups[t.groupId] = groups[t.groupId] || [];
83 | groups[t.groupId].push(tab.id);
84 | }
85 | }
86 | }
87 | else {
88 | const windows = {};
89 | tabs.forEach(t => {
90 | windows[t.windowId] = windows[t.windowId] || [];
91 | windows[t.windowId].push(t);
92 | });
93 | // sort
94 | Object.keys(windows).forEach(id => windows[id].sort((a, b) => a.index - b.index));
95 | // restore
96 | for (const id of Object.keys(windows)) {
97 | const tab = windows[id][0];
98 | const props = {
99 | incognito: tab.incognito
100 | };
101 | if ('window' in tab && tab.window.width) {
102 | props.left = tab.window.left;
103 | props.top = tab.window.top;
104 | props.width = tab.window.width;
105 | props.height = tab.window.height;
106 | }
107 | const win = await new Promise(resolve => chrome.windows.create(props, resolve));
108 | const toberemoved = win.tabs;
109 | for (const t of windows[id]) {
110 | const props = {
111 | pinned: t.pinned,
112 | active: t.active,
113 | windowId: win.id,
114 | index: t.index
115 | };
116 | if ('cookieStoreId' in t) {
117 | props.cookieStoreId = t.cookieStoreId;
118 | }
119 | const tab = await create(t, props);
120 | if ('groupId' in t) {
121 | groups[t.groupId] = groups[t.groupId] || [];
122 | groups[t.groupId].windowId = win.id;
123 | groups[t.groupId].push(tab.id);
124 | }
125 | }
126 | for (const {id} of toberemoved) {
127 | chrome.tabs.remove(id);
128 | }
129 | }
130 | }
131 | if ('group' in chrome.tabs) {
132 | for (let groupId of Object.keys(groups)) {
133 | groupId = Number(groupId);
134 | if (isNaN(groupId) === false && groupId > -1) {
135 | chrome.tabs.group({
136 | createProperties: {
137 | windowId: groups[groupId].windowId
138 | },
139 | tabIds: groups[groupId]
140 | });
141 | }
142 | }
143 | }
144 | if (request.remove && session.permanent !== true) {
145 | const index = prefs.sessions.indexOf(request.session);
146 | prefs.sessions.splice(index, 1);
147 | await storage.set({
148 | sessions: prefs.sessions
149 | });
150 | await storage.remove(request.session);
151 | }
152 | if (removeTabs.length) {
153 | chrome.tabs.remove(removeTabs.map(t => t.id));
154 | }
155 | }
156 | catch (e) {
157 | console.error(e);
158 | notify('Cannot restore tabs. Wrong password?');
159 | response(false);
160 | }
161 | });
162 |
163 | return request.method === 'restore' ? false : true;
164 | }
165 | });
166 |
167 | // recording
168 | const recording = {
169 | async disk(tabs, request, type = 'new') {
170 | const windowIds = new Set(tabs.map(t => t.windowId));
171 | const map = new Map();
172 | for (const windowId of windowIds) {
173 | const win = await new Promise(resolve => chrome.windows.get(windowId, resolve));
174 | map.set(windowId, win);
175 | }
176 |
177 | let json = JSON.stringify(tabs.map(t => {
178 | let url = t.url;
179 | if (url.startsWith('chrome-extension://') && url.indexOf(chrome.runtime.id) !== -1) {
180 | url = (new URLSearchParams(url.split('?')[1])).get('href');
181 | }
182 | const win = map.get(t.windowId);
183 | return {
184 | url,
185 | title: t.title,
186 | active: t.active,
187 | pinned: t.pinned,
188 | incognito: t.incognito,
189 | index: t.index,
190 | windowId: t.windowId,
191 | window: {
192 | focused: win.focused,
193 | type: win.type,
194 | left: win.left,
195 | top: win.top,
196 | width: win.width,
197 | height: win.height
198 | },
199 | cookieStoreId: t.cookieStoreId,
200 | groupId: t.groupId
201 | };
202 | }));
203 | if (request.password) {
204 | json = await safe.encrypt(json, request.password);
205 | }
206 | const name = type === 'new' ? 'session.' + request.name : request.session;
207 | const prefs = await storage.get({
208 | sessions: [],
209 | [name]: {}
210 | });
211 | if (type === 'new') {
212 | prefs.sessions.push(name);
213 | }
214 | Object.assign(prefs[name], {
215 | json,
216 | timestamp: Date.now(),
217 | tabs: tabs.length
218 | });
219 | if (type === 'new') {
220 | Object.assign(prefs[name], {
221 | permanent: request.permanent,
222 | protected: Boolean(request.password),
223 | query: {
224 | rule: request.rule,
225 | pinned: request.pinned,
226 | internal: request.internal
227 | }
228 | });
229 | }
230 | await storage.set(prefs);
231 | },
232 | perform(request, response, type = 'new') {
233 | const props = {
234 | windowType: 'normal'
235 | };
236 | if (request.rule.startsWith('save-window')) {
237 | props.currentWindow = true;
238 | }
239 | if (request.rule.startsWith('save-other-windows')) {
240 | props.currentWindow = false;
241 | }
242 | if (request.pinned === false) {
243 | props.pinned = false;
244 | }
245 | chrome.tabs.query(props, async tabs => {
246 | if (request.internal !== true) {
247 | tabs = tabs.filter(
248 | ({url}) => url &&
249 | url.startsWith('file://') === false &&
250 | url.startsWith('chrome://') === false &&
251 | (
252 | url.startsWith('chrome-extension://') === false ||
253 | (url.startsWith('chrome-extension://') && url.indexOf(chrome.runtime.id) !== -1)
254 | ) &&
255 | url.startsWith('moz-extension://') === false &&
256 | url.startsWith('about:') === false
257 | );
258 | }
259 | if (tabs.length === 0) {
260 | notify('nothing to save');
261 | return response(false);
262 | }
263 | await recording.disk(tabs, request, type);
264 | if (request.rule === 'save-tabs-close') {
265 | chrome.tabs.create({
266 | url: 'about:blank'
267 | }, () => chrome.tabs.remove(tabs.map(t => t.id)));
268 | }
269 | else if (request.rule.endsWith('-close')) {
270 | chrome.tabs.remove(tabs.map(t => t.id));
271 | }
272 | response(tabs.length);
273 | });
274 | }
275 | };
276 |
277 | // context menu
278 | {
279 | const onstartup = () => {
280 | chrome.contextMenus.create({
281 | title: 'Append JSON sessions',
282 | id: 'append',
283 | contexts: ['browser_action']
284 | });
285 | chrome.contextMenus.create({
286 | title: 'Overwrite JSON sessions',
287 | id: 'overwrite',
288 | contexts: ['browser_action']
289 | });
290 | chrome.contextMenus.create({
291 | title: 'Export as JSON',
292 | id: 'export',
293 | contexts: ['browser_action']
294 | });
295 | };
296 | chrome.runtime.onStartup.addListener(onstartup);
297 | chrome.runtime.onInstalled.addListener(onstartup);
298 | }
299 | chrome.contextMenus.onClicked.addListener(info => {
300 | if (info.menuItemId === 'export') {
301 | storage.get(null).then(prefs => {
302 | const text = JSON.stringify(prefs, null, '\t');
303 | const blob = new Blob([text], {type: 'application/json'});
304 | const objectURL = URL.createObjectURL(blob);
305 | Object.assign(document.createElement('a'), {
306 | href: objectURL,
307 | type: 'application/json',
308 | download: 'save-tabs-sessions.json'
309 | }).dispatchEvent(new MouseEvent('click'));
310 | setTimeout(() => URL.revokeObjectURL(objectURL));
311 | });
312 | }
313 | else if (info.menuItemId === 'append' || info.menuItemId === 'overwrite') {
314 | chrome.windows.create({
315 | url: 'data/drop/index.html?command=' + info.menuItemId,
316 | width: 600,
317 | height: 300,
318 | left: screen.availLeft + Math.round((screen.availWidth - 600) / 2),
319 | top: screen.availTop + Math.round((screen.availHeight - 300) / 2),
320 | type: 'popup'
321 | });
322 | }
323 | });
324 |
325 | /* FAQs & Feedback */
326 | {
327 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome;
328 | if (navigator.webdriver !== true) {
329 | const page = getManifest().homepage_url;
330 | const {name, version} = getManifest();
331 | onInstalled.addListener(({reason, previousVersion}) => {
332 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({
333 | 'faqs': true,
334 | 'last-update': 0
335 | }, prefs => {
336 | if (reason === 'install' || (prefs.faqs && reason === 'update')) {
337 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45;
338 | if (doUpdate && previousVersion !== version) {
339 | tabs.query({active: true, currentWindow: true}, tbs => tabs.create({
340 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason,
341 | active: reason === 'install',
342 | ...(tbs && tbs.length && {index: tbs[0].index + 1})
343 | }));
344 | storage.local.set({'last-update': Date.now()});
345 | }
346 | }
347 | }));
348 | });
349 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version);
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/v3/data/popup/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (location.href.indexOf('mode=vertical') !== -1) {
4 | document.body.classList.add('vertical');
5 | }
6 |
7 | self.counter = number => {
8 | return number;
9 | };
10 |
11 | const prompt = document.getElementById('prompt');
12 | const ask = (msg, type = 'password', value = '') => new Promise((resolve, reject) => {
13 | const password = prompt.querySelector('[type=password]');
14 | const text = prompt.querySelector('[type=text]');
15 | const callback = e => {
16 | e.preventDefault();
17 | prompt.reject = '';
18 | prompt.removeEventListener('submit', callback);
19 | if (type === 'password') {
20 | resolve(password.value);
21 | }
22 | else if (type === 'prompt') {
23 | resolve(text.value);
24 | }
25 | else {
26 | resolve(true);
27 | }
28 | prompt.close();
29 | };
30 | prompt.querySelector('span').textContent = msg;
31 | prompt.dataset.type = type;
32 | password[type === 'password' ? 'setAttribute' : 'removeAttribute']('required', true);
33 | text[type === 'prompt' ? 'setAttribute' : 'removeAttribute']('required', true);
34 | prompt.addEventListener('submit', callback);
35 | prompt.reject = reject;
36 | if (type === 'password') {
37 | password.focus();
38 | password.value = value;
39 | }
40 | else if (type === 'prompt') {
41 | text.focus();
42 | text.value = value;
43 | }
44 | prompt.showModal();
45 | });
46 | prompt.addEventListener('click', ({target}) => {
47 | if (target === prompt) {
48 | prompt.close();
49 | if (prompt.reject) {
50 | prompt.reject();
51 | }
52 | }
53 | });
54 | prompt.querySelector('input[type=button]').addEventListener('click', () => {
55 | prompt.dispatchEvent(new Event('click'));
56 | });
57 | prompt.addEventListener('keydown', e => {
58 | if (e.code === 'Escape') {
59 | e.preventDefault();
60 | e.stopPropagation();
61 | prompt.close();
62 | }
63 | });
64 |
65 | document.addEventListener('click', async e => {
66 | const target = e.target;
67 | const method = target.dataset.cmd;
68 | if (method === 'remove') {
69 | await ask('The session data will be erased. Are you sure?', 'confirm');
70 |
71 | const div = target.closest('div[data-session]');
72 | const {session} = div.dataset;
73 |
74 | chrome.storage.sync.get({
75 | sessions: []
76 | }, p1 => {
77 | chrome.storage.local.get({
78 | sessions: []
79 | }, p2 => {
80 | const next = minus => {
81 | document.body.dataset.count = p1.sessions.length + p2.sessions.length - minus;
82 | div.remove();
83 | };
84 |
85 | if (p1.sessions.includes(session)) {
86 | chrome.storage.sync.set({
87 | sessions: p1.sessions.filter(a => a !== session)
88 | }, () => chrome.storage.sync.remove(session, () => {
89 | next(1);
90 | }));
91 | }
92 | else {
93 | if (p2.sessions.includes(session)) {
94 | chrome.storage.local.set({
95 | sessions: p2.sessions.filter(a => a !== session)
96 | }, () => chrome.storage.local.remove(session, () => {
97 | next(1);
98 | }));
99 | }
100 | else {
101 | next(0);
102 | }
103 | }
104 | });
105 | });
106 | }
107 | else if (method === 'rename') {
108 | const div = target.closest('div[data-session]');
109 | const type = div.dataset.synced === 'true' ? 'sync' : 'local';
110 | const session = div.dataset.session;
111 | const e = div.querySelector('span');
112 | chrome.storage[type].get({
113 | [session]: {}
114 | }).then(async prefs => {
115 | const name = await ask('Rename this session?', 'prompt', e.title);
116 |
117 | if (name) {
118 | prefs[session].name = name;
119 | e.title = e.textContent = name;
120 | chrome.storage[type].set(prefs);
121 | }
122 | });
123 | }
124 | else if (method === 'preview') {
125 | const div = target.closest('div[data-session]');
126 | const {locked, session, permanent} = div.dataset;
127 |
128 | const password = locked === 'true' ? await ask('Enter the Session Password', 'password') : '';
129 |
130 | const dialog = document.getElementById('popup');
131 | const iframe = dialog.querySelector('iframe');
132 | iframe.addEventListener('load', () => chrome.runtime.sendMessage({
133 | method,
134 | session,
135 | password
136 | }, tabs => {
137 | if (Array.isArray(tabs)) {
138 | iframe.contentWindow.build({
139 | tabs,
140 | password,
141 | session,
142 | permanent: permanent === 'true',
143 | div
144 | });
145 | }
146 | else {
147 | dialog.close();
148 | delete iframe.onload;
149 | }
150 | }), {
151 | once: true
152 | });
153 | dialog.showModal();
154 | iframe.src = '/data/editor/index.html';
155 | }
156 | else if (method === 'restore') {
157 | const div = target.closest('div[data-session]');
158 | const {locked, session, permanent} = div.dataset;
159 | chrome.runtime.sendMessage({
160 | method,
161 | session,
162 | password: locked === 'true' ? await ask('Enter the Session Password', 'password') : '',
163 | remove: e.shiftKey === false,
164 | single: document.getElementById('single').checked,
165 | discard: document.getElementById('discard').checked,
166 | clean: document.getElementById('clean').checked
167 | }, () => {
168 | if (permanent !== 'true') {
169 | window.close();
170 | div.remove();
171 | }
172 | });
173 | }
174 | else if (method && method.startsWith('save-')) {
175 | const dialog = document.getElementById('popup');
176 | const iframe = dialog.querySelector('iframe');
177 | iframe.src = '/data/dialog/index.html?method=' + method + '&silent=' + document.getElementById('silent').checked;
178 | dialog.showModal();
179 | }
180 | else if (method === 'overwrite') {
181 | await ask('Your session will be overwritten by open tabs. Are you sure?', 'confirm');
182 | e.target.classList.add('disabled');
183 | const div = target.closest('div[data-session]');
184 | const {session} = div.dataset;
185 | chrome.storage.local.get({
186 | 'pinned': true,
187 | 'internal': false
188 | }, prefs => chrome.runtime.sendMessage({
189 | method: 'overwrite',
190 | session,
191 | rule: 'save-tabs',
192 | ...prefs
193 | }, o => {
194 | if ('error' in o) {
195 | alert(o.error);
196 | }
197 | else {
198 | e.target.classList.remove('disabled');
199 | if (o.count) {
200 | e.target.closest('[data-session]').querySelector('[data-id=count]').textContent = self.counter(o.count);
201 | }
202 | }
203 | }));
204 | }
205 | else if (method) {
206 | chrome.runtime.sendMessage({
207 | method
208 | });
209 | }
210 | });
211 |
212 | const format = num => {
213 | const d = new Date(num);
214 | const yyyy = d.getFullYear().toString();
215 |
216 | return `${yyyy.substr(-2)}.${('00' + (d.getMonth() + 1)).substr(-2)}.${('00' + d.getDate()).substr(-2)} ` +
217 | `${('00' + d.getHours()).substr(-2)}:${('00' + d.getMinutes()).substr(-2)}`;
218 | };
219 |
220 | const build = () => chrome.storage.sync.get(null, p1 => {
221 | p1.sessions = p1.sessions || [];
222 | chrome.storage.local.get(null, p2 => {
223 | p2.sessions = p2.sessions || [];
224 |
225 | const sessions = document.getElementById('sessions');
226 | sessions.textContent = '';
227 | const f = document.createDocumentFragment();
228 |
229 | const a = [...p1.sessions, ...p2.sessions];
230 | a.sort((a, b) => {
231 | const oa = p1[a] || p2[a] || {};
232 | const ob = p1[b] || p2[b] || {};
233 |
234 | return oa.timestamp - ob.timestamp;
235 | });
236 |
237 | document.body.dataset.count = a.length;
238 |
239 | // preview
240 | {
241 | const p1 = document.createElement('span');
242 | p1.textContent = 'Name';
243 | const p3 = document.createElement('span');
244 | p3.textContent = 'Permanent';
245 | p3.title = `Y: Permanent Session
246 | N: Temporary Session`;
247 | const p2 = document.createElement('span');
248 | p2.textContent = 'Storage';
249 | p2.title = `S: Stored in the synced storage
250 | L: Stored in the local storage`;
251 | const p4 = document.createElement('span');
252 | p4.textContent = 'Locked';
253 | p4.title = `Session is password protected or not`;
254 | const p5 = document.createElement('span');
255 | p5.textContent = 'Tabs';
256 | const p6 = document.createElement('span');
257 | p6.textContent = '-';
258 | const p7 = document.createElement('span');
259 | p7.textContent = 'Date';
260 | const p8 = document.createElement('span');
261 | p8.textContent = '-';
262 | const p9 = document.createElement('span');
263 | p9.textContent = '-';
264 | const p10 = document.createElement('span');
265 | p10.textContent = '-';
266 |
267 | const div = document.createElement('div');
268 | div.classList.add('header');
269 | div.append(p1, p2, p3, p4, p5, p6, p7, p8, p9, p10);
270 | sessions.appendChild(div);
271 | }
272 |
273 | a.forEach(session => {
274 | const obj = p1[session] || p2[session];
275 | if (!obj) {
276 | console.info('session skipped', session, p1, p2);
277 | return;
278 | }
279 |
280 | // fix password protected
281 | obj.protected = obj.json.startsWith('data:application/octet-binary;');
282 |
283 | const div = document.createElement('div');
284 | div.dataset.session = session;
285 | div.dataset.locked = obj.protected;
286 | div.dataset.synced = p1.sessions.includes(session);
287 | const name = document.createElement('span');
288 | name.textContent = obj.name || session.replace(/^session\./, '');
289 | name.dataset.session = session;
290 | name.dataset.cmd = 'restore';
291 | name.title = name.textContent;
292 | if (!obj.permanent) {
293 | name.title += `
294 |
295 | Shift + click: restore without removing the session`;
296 | }
297 | div.appendChild(name);
298 |
299 | const synced = document.createElement('span');
300 | synced.dataset.id = 'synced';
301 | synced.textContent = p1.sessions.includes(session) ? 'S' : 'L';
302 | synced.title = p1.sessions.includes(session) ? 'Stored in the synced storage' : 'Stored in the local storage';
303 | div.appendChild(synced);
304 |
305 | const permanent = document.createElement('span');
306 | permanent.dataset.id = 'permanent';
307 | permanent.textContent = obj.permanent ? 'Y' : 'N';
308 | permanent.title = obj.permanent ? 'This session is permanent' : 'This session is temporary';
309 | div.appendChild(permanent);
310 |
311 | const lock = document.createElement('span');
312 | lock.dataset.id = 'lock';
313 | lock.title = obj.protected ? 'This session is password protected' : 'This session is not protected';
314 | div.appendChild(lock);
315 | const number = document.createElement('span');
316 | number.dataset.id = 'count';
317 | number.textContent = self.counter(obj.tabs);
318 | div.appendChild(number);
319 | const preview = document.createElement('span');
320 | preview.dataset.cmd = 'preview';
321 | preview.title = 'Preview this Session';
322 | div.appendChild(preview);
323 | const date = document.createElement('span');
324 | date.textContent = format(obj.timestamp);
325 | div.appendChild(date);
326 | const overwrite = document.createElement('span');
327 | overwrite.dataset.cmd = 'overwrite';
328 | overwrite.title = 'Overwrite this Session with Open Tabs';
329 | div.appendChild(overwrite);
330 | const rename = document.createElement('span');
331 | rename.dataset.cmd = 'rename';
332 | rename.title = 'Rename this Session';
333 | div.appendChild(rename);
334 | const close = document.createElement('span');
335 | close.dataset.cmd = 'remove';
336 | close.title = 'Remove this Session';
337 | div.appendChild(close);
338 | div.dataset.permanent = obj.permanent;
339 | f.appendChild(div);
340 | });
341 | sessions.appendChild(f);
342 | sessions.scrollTop = sessions.scrollHeight;
343 | });
344 | });
345 | window.build = build;
346 | document.addEventListener('DOMContentLoaded', build);
347 |
348 | // persist
349 | document.addEventListener('DOMContentLoaded', () => chrome.storage.local.get({
350 | 'silent': false,
351 | 'single': false,
352 | 'discard': false,
353 | 'clean': false
354 | }, prefs => {
355 | document.getElementById('silent').checked = prefs.silent;
356 | document.getElementById('single').checked = prefs.single;
357 | document.getElementById('normal').checked = prefs.single === false;
358 | document.getElementById('discard').checked = prefs.discard;
359 | document.getElementById('clean').checked = prefs.clean;
360 | }));
361 | document.getElementById('silent').addEventListener('change', e => chrome.storage.local.set({
362 | 'silent': e.target.checked
363 | }));
364 | document.getElementById('manager').addEventListener('change', () => chrome.storage.local.set({
365 | 'single': document.getElementById('single').checked,
366 | 'discard': document.getElementById('discard').checked,
367 | 'clean': document.getElementById('clean').checked
368 | }));
369 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
--------------------------------------------------------------------------------
/v3/worker.js:
--------------------------------------------------------------------------------
1 | /* global safe */
2 |
3 | if (typeof importScripts !== 'undefined') {
4 | self.importScripts('safe.js');
5 | }
6 |
7 | const notify = message => chrome.notifications.create({
8 | type: 'basic',
9 | title: chrome.runtime.getManifest().name,
10 | message,
11 | iconUrl: '/data/icons/48.png'
12 | }, id => {
13 | setTimeout(() => chrome.notifications.clear(id), 5000);
14 | });
15 |
16 | chrome.runtime.onMessage.addListener((request, sender, response) => {
17 | if (request.method === 'store' || request.method === 'overwrite') {
18 | recording.perform(request, response, request.method === 'store' ? 'new' : 'update');
19 | return true;
20 | }
21 | else if (request.method === 'update') {
22 | recording.disk(request.tabs, request, 'update');
23 | response(true);
24 | }
25 | else if (request.method === 'restore' || request.method === 'preview') {
26 | const next = async (session, sessions, type) => {
27 | session.protected = session.json.startsWith('data:application/octet-binary;');
28 | try {
29 | const content = session.protected ? await safe.decrypt(session.json, request.password) : session.json;
30 | let tabs;
31 | try {
32 | tabs = JSON.parse(content);
33 | }
34 | // backup plan for old version of "safe.js" that does not handle non-printable characters
35 | catch (e) {
36 | tabs = JSON.parse(content.replace(/[^\x20-\x7E]/g, ''));
37 | }
38 |
39 | if (request.method === 'preview') {
40 | return response(tabs);
41 | }
42 | // remove currents
43 | const removeTabs = [];
44 | if (request.clean) {
45 | await chrome.tabs.query({}).then(tabs => removeTabs.push(...tabs));
46 | }
47 | // restore
48 | const create = async (tab, props) => {
49 | const discarded = request.discard && tab.active !== true;
50 | if (/Firefox/.test(navigator.userAgent)) {
51 | props = {...props, discarded, url: tab.url};
52 |
53 | if ('cookieStoreId' in props) {
54 | const v = await chrome.permissions.contains({
55 | permissions: ['cookies']
56 | });
57 | if (v === false) {
58 | delete props.cookieStoreId;
59 | }
60 | }
61 |
62 | if (discarded) {
63 | props.title = tab.title;
64 | }
65 | return chrome.tabs.create(props);
66 | }
67 | else {
68 | let url = tab.url || 'about:blank';
69 | if (discarded && url.startsWith('http')) {
70 | url = chrome.runtime.getURL('/data/discard/index.html?href=' +
71 | encodeURIComponent(tab.url)) + '&title=' + encodeURIComponent(tab.title);
72 | }
73 | // in case of importing from Firefox
74 | delete props.cookieStoreId;
75 | return chrome.tabs.create({
76 | ...props,
77 | url
78 | });
79 | }
80 | };
81 | const groups = {};
82 | const groupp = {};
83 | if (request.single) {
84 | for (const t of tabs) {
85 | const props = {
86 | pinned: t.pinned,
87 | active: t.active
88 | };
89 | if ('cookieStoreId' in t) {
90 | props.cookieStoreId = t.cookieStoreId;
91 | }
92 | try {
93 | const tab = await create(t, props);
94 | if ('groupId' in t) {
95 | groups[t.groupId] = groups[t.groupId] || [];
96 | groups[t.groupId].push(tab.id);
97 | groupp[t.groupId] = t.group;
98 | }
99 | }
100 | catch (e) {
101 | console.error('[error]', e);
102 | }
103 | }
104 | }
105 | else {
106 | const windows = {};
107 | tabs.forEach(t => {
108 | windows[t.windowId] = windows[t.windowId] || [];
109 | windows[t.windowId].push(t);
110 | });
111 | // sort
112 | Object.keys(windows).forEach(id => windows[id].sort((a, b) => a.index - b.index));
113 | // restore
114 | for (const id of Object.keys(windows)) {
115 | const tab = windows[id][0];
116 | const props = {
117 | incognito: tab.incognito
118 | };
119 | if ('window' in tab && tab.window.width) {
120 | props.left = tab.window.left;
121 | props.top = tab.window.top;
122 | props.width = tab.window.width;
123 | props.height = tab.window.height;
124 | }
125 | const win = await chrome.windows.create(props);
126 |
127 | const toberemoved = win.tabs;
128 | for (const t of windows[id]) {
129 | const props = {
130 | pinned: t.pinned,
131 | active: t.active,
132 | windowId: win.id,
133 | index: t.index
134 | };
135 | if ('cookieStoreId' in t) {
136 | props.cookieStoreId = t.cookieStoreId;
137 | }
138 | try {
139 | const tab = await create(t, props);
140 | if ('groupId' in t) {
141 | groups[t.groupId] = groups[t.groupId] || [];
142 | groups[t.groupId].windowId = win.id;
143 | groups[t.groupId].push(tab.id);
144 | groupp[t.groupId] = t.group;
145 | }
146 | }
147 | catch (e) {
148 | console.error('[error]', e);
149 | }
150 | }
151 | for (const {id} of toberemoved) {
152 | chrome.tabs.remove(id);
153 | }
154 | }
155 | }
156 | if ('group' in chrome.tabs) {
157 | for (let groupId of Object.keys(groups)) {
158 | groupId = Number(groupId);
159 | if (isNaN(groupId) === false && groupId > -1) {
160 | chrome.tabs.group({
161 | createProperties: {
162 | windowId: groups[groupId].windowId
163 | },
164 | tabIds: groups[groupId]
165 | }, id => {
166 | const p = groupp[groupId];
167 | if (p) {
168 | chrome.tabGroups.update(id, p);
169 | }
170 | });
171 | }
172 | }
173 | }
174 | if (request.remove && session.permanent !== true) {
175 | await chrome.storage[type].set({
176 | sessions: sessions.filter(a => a !== request.session)
177 | });
178 | await chrome.storage[type].remove(request.session);
179 | }
180 | if (removeTabs.length) {
181 | chrome.tabs.remove(removeTabs.map(t => t.id));
182 | }
183 | }
184 | catch (e) {
185 | console.error(e);
186 | notify('Cannot restore tabs. Wrong password or ' + e.message);
187 | response(false);
188 | }
189 | };
190 |
191 | chrome.storage.sync.get({
192 | 'sessions': []
193 | }, prefs => {
194 | if (prefs.sessions.includes(request.session)) {
195 | chrome.storage.sync.get({
196 | [request.session]: {}
197 | }, ps => {
198 | next(ps[request.session], prefs.sessions, 'sync');
199 | });
200 | }
201 | else {
202 | chrome.storage.local.get({
203 | [request.session]: {}
204 | }, ps => {
205 | next(ps[request.session], prefs.sessions, 'local');
206 | });
207 | }
208 | });
209 |
210 | return request.method === 'restore' ? false : true;
211 | }
212 | });
213 |
214 | // recording
215 | const recording = {
216 | async disk(tabs, request, type = 'new') {
217 | const map = new Map();
218 | for (const tab of tabs) {
219 | map.set(tab.windowId, tab.window);
220 | }
221 |
222 | for (const [windowId, oldWin] of map.entries()) {
223 | // what if we are sorting old list (tabs are not accessible anymore)
224 | const win = await chrome.windows.get(windowId).catch(() => (oldWin || {}));
225 | map.set(windowId, win);
226 | }
227 |
228 | const o = [];
229 | for (const t of tabs) {
230 | let url = t.url;
231 | if (url.startsWith('chrome-extension://') && url.includes(chrome.runtime.id)) {
232 | url = (new URLSearchParams(url.split('?')[1])).get('href');
233 | }
234 | const win = map.get(t.windowId);
235 | const me = {
236 | url,
237 | title: t.title,
238 | active: t.active,
239 | pinned: t.pinned,
240 | incognito: t.incognito,
241 | index: t.index,
242 | windowId: t.windowId,
243 | window: {
244 | focused: win.focused,
245 | type: win.type,
246 | left: win.left,
247 | top: win.top,
248 | width: win.width,
249 | height: win.height
250 | },
251 | cookieStoreId: t.cookieStoreId,
252 | groupId: t.groupId
253 | };
254 | if (me.groupId && me.groupId > -1) {
255 | try {
256 | const p = await chrome.tabGroups.get(me.groupId);
257 | me.group = {
258 | collapsed: p.collapsed,
259 | title: p.title,
260 | color: p.color
261 | };
262 | }
263 | catch (e) {}
264 | }
265 | o.push(me);
266 | }
267 | let json = JSON.stringify(o);
268 |
269 | if (request.password) {
270 | json = await safe.encrypt(json, request.password);
271 | }
272 | const name = type === 'new' ? 'session.' + request.name : request.session;
273 |
274 | const psync = await chrome.storage.sync.get({
275 | sessions: []
276 | });
277 | const plocal = await chrome.storage.local.get({
278 | sessions: []
279 | });
280 |
281 | const no = {
282 | timestamp: Date.now()
283 | };
284 |
285 | if (psync.sessions.includes(name)) {
286 | const o = (await chrome.storage.sync.get({
287 | [name]: {}
288 | }))[name];
289 |
290 | Object.assign(no, o);
291 | }
292 | else if (plocal.sessions.includes(name)) {
293 | const o = (await chrome.storage.local.get({
294 | [name]: {}
295 | }))[name];
296 |
297 | Object.assign(no, o);
298 | }
299 |
300 | Object.assign(no, {
301 | json,
302 | tabs: tabs.length
303 | });
304 | if (type === 'new') {
305 | Object.assign(no, {
306 | permanent: request.permanent,
307 | protected: Boolean(request.password),
308 | query: {
309 | rule: request.rule,
310 | pinned: request.pinned,
311 | internal: request.internal
312 | }
313 | });
314 | }
315 |
316 | // try to store in the synced storage if failed, store in the local storage
317 | try {
318 | await chrome.storage.sync.set({
319 | [name]: no
320 | });
321 | // stored before in the wrong storage
322 | if (plocal.sessions.includes(name)) {
323 | const n = plocal.sessions.indexOf(name);
324 | plocal.sessions.splice(n, 1);
325 |
326 | await chrome.storage.local.remove(name);
327 | await chrome.storage.local.set({
328 | sessions: plocal.sessions
329 | });
330 | }
331 |
332 | if (psync.sessions.includes(name) === false) {
333 | await chrome.storage.sync.set({
334 | sessions: [...psync.sessions, name]
335 | });
336 | }
337 | return {
338 | count: tabs.length,
339 | storage: 'sync',
340 | new: false,
341 | origin: 'local.6'
342 | };
343 | }
344 | catch (e) {
345 | await chrome.storage.local.set({
346 | [name]: no
347 | });
348 | // already stored in the wrong storage
349 | if (psync.sessions.includes(name)) {
350 | const n = psync.sessions.indexOf(name);
351 | psync.sessions.splice(n, 1);
352 |
353 | await chrome.storage.sync.set({
354 | sessions: psync.sessions
355 | });
356 | await chrome.storage.sync.remove(name);
357 | }
358 | if (plocal.sessions.includes(name) === false) {
359 | await chrome.storage.local.set({
360 | sessions: [...plocal.sessions, name]
361 | });
362 | }
363 | return {
364 | count: tabs.length,
365 | storage: 'local',
366 | new: false,
367 | origin: 'sync.1'
368 | };
369 | }
370 | },
371 | perform(request, response, type = 'new') {
372 | const props = {
373 | windowType: 'normal'
374 | };
375 | if (request.rule.startsWith('save-window')) {
376 | props.currentWindow = true;
377 | }
378 | if (request.rule.startsWith('save-selected')) {
379 | props.highlighted = true;
380 | }
381 | if (request.rule.startsWith('save-other-windows')) {
382 | props.currentWindow = false;
383 | }
384 | if (request.pinned === false) {
385 | props.pinned = false;
386 | }
387 | chrome.tabs.query(props, async tabs => {
388 | try {
389 | if (request.internal !== true) {
390 | tabs = tabs.filter(
391 | ({url}) => url &&
392 | url.startsWith('file://') === false &&
393 | url.startsWith('chrome://') === false &&
394 | (
395 | url.startsWith('chrome-extension://') === false ||
396 | (url.startsWith('chrome-extension://') && url.includes(chrome.runtime.id))
397 | ) &&
398 | url.startsWith('moz-extension://') === false &&
399 | url.startsWith('about:') === false
400 | );
401 | }
402 | if (tabs.length === 0) {
403 | notify('nothing to save');
404 | return response({
405 | count: 0
406 | });
407 | }
408 | const r = await recording.disk(tabs, request, type);
409 | if (request.rule === 'save-tabs-close') {
410 | chrome.tabs.create({
411 | url: 'about:blank'
412 | }, () => chrome.tabs.remove(tabs.map(t => t.id)));
413 | }
414 | else if (request.rule.endsWith('-close')) {
415 | chrome.tabs.remove(tabs.map(t => t.id));
416 | }
417 | response(r);
418 | }
419 | catch (e) {
420 | console.error(e);
421 | response({
422 | error: e.message
423 | });
424 | }
425 | });
426 | }
427 | };
428 |
429 | // context menu
430 | {
431 | const onstartup = () => {
432 | if (onstartup.done) {
433 | return;
434 | }
435 | onstartup.done = true;
436 |
437 | chrome.contextMenus.create({
438 | title: 'Add Sessions from JSON File to Current List',
439 | id: 'append',
440 | contexts: ['action']
441 | });
442 | chrome.contextMenus.create({
443 | title: 'Overwrite List with Sessions from JSON File',
444 | id: 'overwrite',
445 | contexts: ['action']
446 | });
447 | chrome.contextMenus.create({
448 | title: 'Export Sessions as JSON File',
449 | id: 'export',
450 | contexts: ['action']
451 | });
452 | if (navigator.userAgent.includes('Firefox')) {
453 | chrome.contextMenus.create({
454 | title: 'Permission to use containers',
455 | id: 'containers',
456 | contexts: ['action']
457 | });
458 | }
459 | };
460 | chrome.runtime.onStartup.addListener(onstartup);
461 | chrome.runtime.onInstalled.addListener(onstartup);
462 | }
463 | chrome.contextMenus.onClicked.addListener(async info => {
464 | if (info.menuItemId === 'export') {
465 | const prefs = await chrome.storage.sync.get(null);
466 | prefs.sessions = prefs.sessions || [];
467 |
468 | const ps = await chrome.storage.local.get(null);
469 | if ('sessions' in ps) {
470 | for (const session of ps.sessions) {
471 | prefs[session] = ps[session] || prefs[session];
472 | prefs.sessions.push(session);
473 | }
474 | }
475 | const text = JSON.stringify(prefs, null, ' ');
476 | if (navigator.userAgent.includes('Firefox')) {
477 | const response = new Response(text, {
478 | headers: {
479 | 'Content-Type': 'application/json'
480 | }
481 | });
482 | const blob = await response.blob();
483 | const blobUrl = URL.createObjectURL(blob);
484 | await chrome.downloads.download({
485 | filename: 'save-tabs-sessions.json',
486 | url: blobUrl
487 | });
488 | URL.revokeObjectURL(blobUrl);
489 | }
490 | else {
491 | // Dealing with characters outside of the Latin1 range.
492 | const buffer = await new Response(text).arrayBuffer();
493 | const bytes = new Uint8Array(buffer);
494 | let binary = '';
495 | const chunkSize = 0x8000;
496 | for (let i = 0; i < bytes.length; i += chunkSize) {
497 | binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
498 | }
499 |
500 | chrome.downloads.download({
501 | filename: 'save-tabs-sessions.json',
502 | url: 'data:application/json;base64,' + btoa(binary)
503 | });
504 | }
505 | }
506 | else if (info.menuItemId === 'append' || info.menuItemId === 'overwrite') {
507 | chrome.windows.getCurrent(win => {
508 | chrome.windows.create({
509 | url: '/data/drop/index.html?command=' + info.menuItemId,
510 | width: 600,
511 | height: 300,
512 | left: win.left + Math.round((win.width - 600) / 2),
513 | top: win.top + Math.round((win.height - 300) / 2),
514 | type: 'popup'
515 | });
516 | });
517 | }
518 | else if (info.menuItemId === 'containers') {
519 | chrome.permissions.request({
520 | permissions: ['cookies']
521 | });
522 | }
523 | });
524 |
525 | /* FAQs & Feedback */
526 | {
527 | const {management, runtime: {onInstalled, setUninstallURL, getManifest}, storage, tabs} = chrome;
528 | if (navigator.webdriver !== true) {
529 | const {homepage_url: page, name, version} = getManifest();
530 | onInstalled.addListener(({reason, previousVersion}) => {
531 | management.getSelf(({installType}) => installType === 'normal' && storage.local.get({
532 | 'faqs': true,
533 | 'last-update': 0
534 | }, prefs => {
535 | if (reason === 'install' || (prefs.faqs && reason === 'update')) {
536 | const doUpdate = (Date.now() - prefs['last-update']) / 1000 / 60 / 60 / 24 > 45;
537 | if (doUpdate && previousVersion !== version) {
538 | tabs.query({active: true, lastFocusedWindow: true}, tbs => tabs.create({
539 | url: page + '?version=' + version + (previousVersion ? '&p=' + previousVersion : '') + '&type=' + reason,
540 | active: reason === 'install',
541 | ...(tbs && tbs.length && {index: tbs[0].index + 1})
542 | }));
543 | storage.local.set({'last-update': Date.now()});
544 | }
545 | }
546 | }));
547 | });
548 | setUninstallURL(page + '?rd=feedback&name=' + encodeURIComponent(name) + '&version=' + version);
549 | }
550 | }
551 |
--------------------------------------------------------------------------------