├── .gitignore
├── assets
├── edge.png
├── chrome.png
├── opera.png
├── safari.png
├── firefox.png
├── B612
│ ├── B612-Bold.ttf
│ ├── B612-Italic.ttf
│ ├── B612-Regular.ttf
│ ├── B612-BoldItalic.ttf
│ └── OFL.txt
├── firefox_addon.png
├── chrome_webstore.png
├── screenshots
│ ├── Save_Tabs.PNG
│ ├── Save_Tabs_Logs.PNG
│ ├── Save_Tabs_Export.PNG
│ ├── Save_Tabs_Import.PNG
│ ├── Save_Tabs_Export-Custom.PNG
│ └── Save_Tabs_Import-Grouped.PNG
└── Preview
│ ├── Save_Tabs_Export.png
│ ├── Save_Tabs_Import.png
│ └── Save_Tabs_Logs.png
├── extension
├── icons
│ ├── Save_Tabs.png
│ ├── Save_Tabs_16.png
│ ├── Save_Tabs_32.png
│ ├── Save_Tabs_48.png
│ ├── Save_Tabs_64.png
│ ├── Save_Tabs_96.png
│ └── Save_Tabs_128.png
├── manifest-chrome.json
├── manifest-firefox.json
├── saveTab-chrome.css
├── background.js
├── saveTab.html
├── saveTab.css
└── saveTab.js
├── setup.sh
├── index.js
├── LICENSE
├── README.md
├── index.html
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | firefox/
2 | chrome/
--------------------------------------------------------------------------------
/assets/edge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/edge.png
--------------------------------------------------------------------------------
/assets/chrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/chrome.png
--------------------------------------------------------------------------------
/assets/opera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/opera.png
--------------------------------------------------------------------------------
/assets/safari.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/safari.png
--------------------------------------------------------------------------------
/assets/firefox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/firefox.png
--------------------------------------------------------------------------------
/assets/B612/B612-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/B612/B612-Bold.ttf
--------------------------------------------------------------------------------
/assets/firefox_addon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/firefox_addon.png
--------------------------------------------------------------------------------
/assets/B612/B612-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/B612/B612-Italic.ttf
--------------------------------------------------------------------------------
/assets/chrome_webstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/chrome_webstore.png
--------------------------------------------------------------------------------
/assets/B612/B612-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/B612/B612-Regular.ttf
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs.png
--------------------------------------------------------------------------------
/assets/B612/B612-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/B612/B612-BoldItalic.ttf
--------------------------------------------------------------------------------
/assets/screenshots/Save_Tabs.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/screenshots/Save_Tabs.PNG
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs_16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs_16.png
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs_32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs_32.png
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs_48.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs_48.png
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs_64.png
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs_96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs_96.png
--------------------------------------------------------------------------------
/assets/Preview/Save_Tabs_Export.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/Preview/Save_Tabs_Export.png
--------------------------------------------------------------------------------
/assets/Preview/Save_Tabs_Import.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/Preview/Save_Tabs_Import.png
--------------------------------------------------------------------------------
/assets/Preview/Save_Tabs_Logs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/Preview/Save_Tabs_Logs.png
--------------------------------------------------------------------------------
/extension/icons/Save_Tabs_128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/extension/icons/Save_Tabs_128.png
--------------------------------------------------------------------------------
/assets/screenshots/Save_Tabs_Logs.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/screenshots/Save_Tabs_Logs.PNG
--------------------------------------------------------------------------------
/assets/screenshots/Save_Tabs_Export.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/screenshots/Save_Tabs_Export.PNG
--------------------------------------------------------------------------------
/assets/screenshots/Save_Tabs_Import.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/screenshots/Save_Tabs_Import.PNG
--------------------------------------------------------------------------------
/assets/screenshots/Save_Tabs_Export-Custom.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/screenshots/Save_Tabs_Export-Custom.PNG
--------------------------------------------------------------------------------
/assets/screenshots/Save_Tabs_Import-Grouped.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Karna98/Save-Tabs/HEAD/assets/screenshots/Save_Tabs_Import-Grouped.PNG
--------------------------------------------------------------------------------
/setup.sh:
--------------------------------------------------------------------------------
1 | # Creates chrome folder and copy all required files and folders to it.
2 | # 1. manifest.json (manifest-chrome.json)
3 | # 2. saveTab.html
4 | # 3. saveTab.css
5 | # 4. saveTab.js
6 | # 5. background.js
7 | # 6. icons/
8 | # 7. saveTab-chrome.css
9 | mkdir chrome
10 | cp -R extension/{background.js,saveTab.css,saveTab-chrome.css,saveTab.js,saveTab.html} chrome
11 | cp -R extension/icons chrome
12 | cp extension/manifest-chrome.json chrome/manifest.json
13 | # Creates firefox folder and copy all required files and folders to it.
14 | # 1. manifest.json (manifest-firefox.json)
15 | # 2. saveTab.html
16 | # 3. saveTab.css
17 | # 4. saveTab.js
18 | # 5. background.js
19 | # 6. icons/
20 | mkdir firefox
21 | cp -R extension/{background.js,saveTab.css,saveTab.js,saveTab.html} firefox
22 | cp -R extension/icons firefox
23 | cp extension/manifest-firefox.json firefox/manifest.json
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | `use strict`;
2 |
3 | window.addEventListener(`DOMContentLoaded`, () => {
4 | const hamburger = document.querySelector(".hamburger");
5 | const navMenu = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0];
6 | const header = document.getElementsByTagName("header")[0];
7 | const body = document.getElementsByTagName("body")[0];
8 |
9 | function mobileMenu() {
10 | hamburger.classList.toggle("active");
11 | navMenu.classList.toggle("active");
12 | header.classList.toggle("active");
13 | body.classList.toggle('scroll-lock');
14 | }
15 |
16 | function closeMenu() {
17 | hamburger.classList.remove("active");
18 | navMenu.classList.remove("active");
19 | header.classList.remove("active");
20 | body.classList.remove('scroll-lock');
21 | }
22 |
23 | const navLink = document.querySelectorAll(".nav-link");
24 | navLink.forEach(n => n.addEventListener("click", closeMenu));
25 | hamburger.addEventListener("click", mobileMenu);
26 | });
--------------------------------------------------------------------------------
/extension/manifest-chrome.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 3,
3 | "name": "Save Tabs",
4 | "version": "1.0.1",
5 |
6 | "description": "Export and Import Tabs.",
7 | "homepage_url": "https://github.com/karna98/Save-Tabs",
8 |
9 | "icons": {
10 | "16": "icons/Save_Tabs_16.png",
11 | "32": "icons/Save_Tabs_32.png",
12 | "48": "icons/Save_Tabs_48.png",
13 | "64": "icons/Save_Tabs_64.png",
14 | "96": "icons/Save_Tabs_96.png",
15 | "128": "icons/Save_Tabs_128.png"
16 | },
17 |
18 | "permissions": [
19 | "tabs",
20 | "tabGroups",
21 | "downloads",
22 | "storage"
23 | ],
24 |
25 | "incognito": "split",
26 |
27 | "background": {
28 | "service_worker": "background.js"
29 | },
30 |
31 | "action": {
32 | "default_icon": {
33 | "16": "icons/Save_Tabs_16.png",
34 | "32": "icons/Save_Tabs_32.png",
35 | "48": "icons/Save_Tabs_48.png",
36 | "64": "icons/Save_Tabs_64.png",
37 | "96": "icons/Save_Tabs_96.png",
38 | "128": "icons/Save_Tabs_128.png"
39 | },
40 | "default_title": "Save Tabs",
41 | "default_popup": "saveTab.html"
42 | }
43 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Vedant Wakalkar
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/extension/manifest-firefox.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "Save Tabs",
4 | "version": "1.0.1",
5 |
6 | "description": "Export and Import Tabs.",
7 | "homepage_url": "https://github.com/karna98/Save-Tabs",
8 |
9 | "developer": {
10 | "name": "Vedant Wakalkar",
11 | "url": "https://karna98.github.io"
12 | },
13 |
14 | "icons": {
15 | "16": "icons/Save_Tabs_16.png",
16 | "32": "icons/Save_Tabs_32.png",
17 | "48": "icons/Save_Tabs_48.png",
18 | "64": "icons/Save_Tabs_64.png",
19 | "96": "icons/Save_Tabs_96.png",
20 | "128": "icons/Save_Tabs_128.png"
21 | },
22 |
23 | "permissions": [
24 | "tabs",
25 | "downloads",
26 | "storage"
27 | ],
28 |
29 | // "incognito": "split", /* Bug for 'split' */
30 |
31 | "browser_action": {
32 | "default_icon": {
33 | "16": "icons/Save_Tabs_16.png",
34 | "32": "icons/Save_Tabs_32.png",
35 | "48": "icons/Save_Tabs_48.png",
36 | "64": "icons/Save_Tabs_64.png",
37 | "96": "icons/Save_Tabs_96.png",
38 | "128": "icons/Save_Tabs_128.png"
39 | },
40 | "default_title": "Save Tabs"
41 | },
42 |
43 | "background": {
44 | "scripts":[
45 | "background.js"
46 | ]
47 | },
48 |
49 | "sidebar_action": {
50 | "default_icon": {
51 | "16": "icons/Save_Tabs_16.png",
52 | "32": "icons/Save_Tabs_32.png",
53 | "48": "icons/Save_Tabs_48.png",
54 | "64": "icons/Save_Tabs_64.png",
55 | "96": "icons/Save_Tabs_96.png",
56 | "128": "icons/Save_Tabs_128.png"
57 | },
58 | "default_title": "Save Tabs",
59 | "default_panel": "saveTab.html"
60 | }
61 | }
--------------------------------------------------------------------------------
/assets/B612/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2012 The B612 Project Authors (https://github.com/polarsys/b612)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/extension/saveTab-chrome.css:
--------------------------------------------------------------------------------
1 | @import url(saveTab.css);
2 |
3 | body {
4 | width: 25rem;
5 | height: 37.5rem;
6 | }
7 |
8 | /**
9 | Navigation Bar
10 | */
11 | .navbar {
12 | padding: 0.25rem 0.5rem;
13 | width: calc(100% - 1rem);
14 | }
15 |
16 | .navbar .navbar-content {
17 | padding: 0.25rem 0.5rem;
18 | width: calc(100% - 1rem);
19 | border-radius: 0.25rem;
20 | }
21 |
22 | /* Extension Name and Logo */
23 | .header span {
24 | padding-left: 0.25rem;
25 | font-size: 1.125rem;
26 | }
27 |
28 | /* Extension Support Links */
29 | .navbar-content .links {
30 | width: 6rem;
31 | }
32 |
33 | .links span svg {
34 | width: 1.75rem;
35 | height: 1.75rem;
36 | }
37 |
38 | .export-section, .import-section, .logs-section {
39 | padding: 0.25rem;
40 | }
41 |
42 | .export-section {
43 | height: calc(25% - 0.5rem);
44 | }
45 |
46 | .import-section {
47 | height: calc(17% - 0.5rem);
48 | }
49 |
50 | .logs-section {
51 | height: calc(58% - 0.5rem);
52 | }
53 |
54 | /* Fieldset */
55 | fieldset {
56 | padding: 0.25rem;
57 | height: calc(100% - 0.5rem);
58 | border-radius: 0.25rem;
59 | }
60 |
61 | .fieldset-content input#file-name {
62 | padding: 0 0.5rem;
63 | height: 1.5rem;
64 | border-radius: 0.25rem;
65 | border-bottom: 0.15rem solid var(--color_A_light_2);
66 | }
67 |
68 | input#file-name:focus {
69 | border-bottom: 0.15rem solid var(--color_A);
70 | }
71 |
72 | /* Legend */
73 | legend {
74 | margin-left: 0.25rem;
75 | width: calc(100% - 0.5rem);
76 | border-radius: 0.25rem;
77 | }
78 |
79 | .legend-left .legend-title,.legend-left .legend-element {
80 | padding: 0 0.25rem;
81 | font-size: 1.1rem;
82 | border-radius: 0.25rem;
83 | }
84 |
85 | .legend-left .legend-element {
86 | margin-left: 0.5rem;
87 | }
88 |
89 | .info .section-info {
90 | width: 1.25rem;
91 | height: 1.25rem;
92 | }
93 |
94 | .section-info svg {
95 | width: 1.25rem;
96 | height: 1.25rem;
97 | }
98 |
99 | /* Tooltip */
100 | .tooltip span {
101 | right: 150%;
102 | padding: 0.125rem 0.25rem;
103 | border-radius: 0.25rem;
104 | }
105 |
106 | .tooltip span:after {
107 | margin-top: -0.5rem;
108 | border-width: 0.5rem;
109 | }
110 |
111 | /* Import & Export Button */
112 | .button {
113 | width: 6rem;
114 | height: 1.5rem;
115 | font-size: 0.8rem;
116 | border-radius: 0.25rem;
117 | }
118 |
119 | .button span {
120 | width: 1.25rem;
121 | height: 1.25rem;
122 | }
123 |
124 | span svg {
125 | width: 1.25rem;
126 | height: 1.25rem;
127 | }
128 |
129 | .export {
130 | border: 0.1rem solid var(--color_A);
131 | -webkit-box-shadow: 0 0.125rem 0.125rem var(--color_A_light_2);
132 | box-shadow: 0 0.125rem 0.125rem var(--color_A_light_2);
133 | }
134 |
135 | .export:hover {
136 | -webkit-box-shadow: 0 0.25rem 0.125rem var(--color_A_light_2);
137 | box-shadow: 0 0.25rem 0.125rem var(--color_A_light_2);
138 | }
139 |
140 | .export:active {
141 | -webkit-box-shadow: 0 0 0.125rem var(--color_A_light_2);
142 | box-shadow: 0 0 0.125rem var(--color_A_light_2);
143 | }
144 |
145 | .import {
146 | border: 0.1rem solid var(--color_B);
147 | -webkit-box-shadow: 0 0.125rem 0.125rem var(--color_B_light_2);
148 | box-shadow: 0 0.125rem 0.125rem var(--color_B_light_2);
149 | }
150 |
151 | .import:hover {
152 | -webkit-box-shadow: 0 0.25rem 0.125rem var(--color_B_light_2);
153 | box-shadow: 0 0.25rem 0.125rem var(--color_B_light_2);
154 | }
155 |
156 | .import:active {
157 | -webkit-box-shadow: 0 0 0.125rem var(--color_B_light_2);
158 | box-shadow: 0 0 0.125rem var(--color_B_light_2);
159 | }
160 |
161 | /* Logs Section */
162 | .logs-section fieldset {
163 | overflow: hidden;
164 | }
165 |
166 | .fieldset-content .logs {
167 | padding: 0.25rem;
168 | width: calc(100% - 0.5rem);
169 | height: calc(100% - 0.5rem);
170 | font-size: 0.8rem;
171 | }
172 |
173 | .logs .log {
174 | padding: 0.25rem;
175 | width: 21rem;
176 | }
177 |
178 | .meta-data .timestamp {
179 | width: 6rem;
180 | }
181 |
182 | .meta-data .status {
183 | margin-left: 0.25rem;
184 | padding: 0.062rem 0.25rem;
185 | font-weight: 500;
186 | }
187 |
188 | /* Log Toggle Button */
189 | input[type="checkbox"] + label {
190 | width: 2rem;
191 | height: 1.25rem;
192 | border-radius: 1.25rem;
193 | }
194 |
195 | input[type="checkbox"] + label:after{
196 | top: 0.125rem;
197 | left: 0.125rem;
198 | width: 1rem;
199 | height: 1rem;
200 | }
201 |
202 | input[type="checkbox"]:checked + label:after {
203 | left: calc(100% - 0.125rem);
204 | }
205 |
206 | input[type="checkbox"] + label:active:after {
207 | width: 1.25rem;
208 | }
209 |
210 | /**
211 | Footer
212 | */
213 | .footer {
214 | padding: 0.25rem 0.5rem;
215 | width: calc(100% - 1rem);
216 | }
217 |
218 | .footer .footer-content {
219 | padding: 0.25rem 0.5rem;
220 | width: calc(100% - 1rem);
221 | border-radius: 0.25rem;
222 | }
223 |
224 | .footer-content .developer-name {
225 | padding: 0 0.125rem;
226 | font-size: 0.8rem;
227 | }
228 |
229 | ::-webkit-scrollbar {
230 | width: 0.5rem
231 | }
232 |
233 | ::-webkit-scrollbar-track {
234 | background-color: var(--color_C_light_2);
235 | border-radius: 0.5rem
236 | }
237 |
238 | ::-webkit-scrollbar-thumb {
239 | border-radius: 0.5rem;
240 | -webkit-box-shadow: inset 0 0 0.5rem var(--color_C);
241 | box-shadow: inset 0 0 0.5rem var(--color_C)
242 | }
243 |
244 | /**
245 | Extras
246 | */
247 | .box-shadow {
248 | -webkit-box-shadow: 0 0 0.2rem var(--color_shadow);
249 | box-shadow: 0 0 0.2rem var(--color_shadow);
250 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
7 |
8 | [](https://chrome.google.com/webstore/detail/detail/save-tabs/ljokfgphjbhjheflldgfmjligcmcmhmn) [](https://addons.mozilla.org/firefox/addon/save-tabs/)
9 |
10 |
11 | ## 💡 About
12 |
13 | Save Tab is a browser extension that helps to exports and imports tabs currently opened in the browser window.
14 |
15 | ### For whom?
16 | One who open lots and lots of tabs in a single browser and want to revisit the same sets of tabs after a while.
17 |
18 |
19 | ## 🎯 Features
20 |
21 | ◻️ **Easy Export and Import of Tabs**
22 |
23 | ◻️ **Export tabs with Custom Name**
24 |
25 | ◻️ **Cross Browser Support** _(as of now Chrome and Firefox)_
26 |
27 | ◻️ **Logs Section**
28 |
29 | ◻️ **Export and Import of Grouped Tabs** _(Chrome only)_
30 |
31 |
32 | ## 🌐 Browsers Supported
33 |
34 |
35 |
36 |
37 | ## ⚙️ Install
38 |
39 | ### From Web Store
40 |
41 |
42 |
44 |
45 | ### From Repository
46 |
47 | 1. Clone this repository by executing following command in cmd/terminal
48 |
49 | ```
50 | git clone https://github.com/Karna98/Save-Tabs.git
51 | ```
52 |
53 | OR
54 | Download zip from [here](https://github.com/Karna98/Save-Tabs/archive/refs/heads/main.zip).
55 |
56 | 2. Once successfully cloned or extracted, open **Save-Tabs** folder.
57 |
58 | - **Using `setup.sh`**.
59 |
60 | 1. Open a terminal in Ubuntu or Git Bash within Sa and execute
61 | ```
62 | ./setup.sh
63 | ```
64 |
65 | 2. On successful execution, new folder 'firefox' and 'chrome' with the following structure will be created
66 | ```
67 | - Save-Tabs
68 | - ...
69 | - firefox
70 | - manifest.json (original 'manifest-firefox.json')
71 | - saveTab.html
72 | - saveTab.css
73 | - saveTab.js
74 | - background.js
75 | - icons
76 | - chrome
77 | - manifest.json (original 'manifest-chrome.json')
78 | - saveTab.html
79 | - saveTab.css
80 | - saveTab.js
81 | - background.js
82 | - icons
83 | - saveTab-chrome.css
84 | ```
85 |
86 | **Note** (For Chrome only):
87 | * Open **_chrome/saveTab.html_**, update
88 |
89 | ```
90 |
91 | ```
92 | to
93 | ```
94 |
95 | ```
96 | Save the updated file.
97 |
98 | 3. Then proceed with **Run Extension** (below) based on the browser.
99 |
100 | * **Run Extension**
101 |
102 | - _Firefox_
103 |
104 | 1. Open _Firefox_ browser and visit **_about:debugging#/runtime/this-firefox_**.
105 |
106 | 2. Under **Temporary Extensions**, click on **Load Temporary Add-on..**.
107 | File Explorer opens, navigate to **_Save-Tabs/firefox_** folder and select **_manifest.json_**.
108 |
109 | 3. On successfully loading, **Save Tabs** extension will be listed under **Temporary Extensions**.
110 | 4. Also, the user can use the extension by clicking on the **Save Tabs** extension icon listed on browser toolbar.
111 |
112 | - _Chrome_
113 |
114 | 1. Open _Chrome_ browser and visit **_chrome://extensions/_**.
115 |
116 | 2. Click on **Load Unpacked**.
117 | File Explorer opens, navigate to **_Save-Tabs/chrome_** folder and select **_manifest.json_**.
118 |
119 | 3. On successfully loading, **Save Tabs** extension will be listed.
120 |
121 | 4. User can use the extension by clicking on the **Save Tabs** extension icon listed on browser toolbar.
122 |
123 | Refer [_Manage your Extension_](https://support.google.com/chrome_webstore/answer/2664769?hl=en) to pin extension on the browser toolbar.
124 |
125 | - _Cross Platform_ [ **Only For Development** ]
126 |
127 | Reference : https://github.com/mozilla/web-ext
128 |
129 | 1. Run `npm install --global web-ext`
130 |
131 | 2. Navigate to `extension` directory and create a copy of `manifest-firefox.json` and rename it to `manifest.json`
132 |
133 | 3. Then run `web-ext lint` for linting related issues.
134 |
135 | 4. Finally run following commands for development environment.
136 | - For Firefox instance, `web-ext run --devtools`
137 | - For Chrome instance, `web-ext run --verbose --devtools --target chromium`
138 |
139 | **Note**: In case, if `web-ext` command doesn't work, then try to run with `npx web-ext`.
140 |
141 | ## 📝 Issues and Suggestions
142 |
143 | Please create new [Issue](https://github.com/Karna98/Save-Tabs/issues/new) for :
144 |
145 | - To report an issue.
146 | - Proposing new features
147 | - Discussion related to this project.
148 |
149 |
150 | ## 💻 Contributing
151 |
152 | Contributions are always WELCOME!
153 |
154 | Before sending a Pull Request, please make sure that you're assigned the task on a GitHub issue.
155 |
156 | - If a relevant issue already exists, discuss the issue and get it assigned to yourself on GitHub.
157 | - If no relevant issue exists, open a new issue and get it assigned to yourself on GitHub.
158 | - Please proceed with a Pull Request only after you're assigned.
159 |
160 |
161 | ## ⚠️ License
162 |
163 | [MIT License](LICENSE)
164 |
--------------------------------------------------------------------------------
/extension/background.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Background Script for toggling Side Panel (Firefox).
3 | * @author Vedant Wakalkar
4 | */
5 |
6 | /**
7 | * Firefox 1
8 | * Google Chrome 2
9 | */
10 | let browserDetected = 1;
11 |
12 | // Browser's API Object.
13 | let browserAPI;
14 |
15 | try {
16 | // Firefox
17 | browserAPI = browser;
18 | } catch (error) {
19 | // Chrome
20 | browserDetected = 2;
21 | browserAPI = chrome;
22 | }
23 |
24 | // Save Tabs Settings Object
25 | let saveTabsSettingsObject;
26 |
27 | // Keep track of all Logs and emptied when stored in local storage.
28 | let LoggerQueue;
29 |
30 | /**
31 | * Logging error or success message.
32 | * @version 1.0.0
33 | * @param {Object} delta Object returned from Promise.
34 | * @param {String} messageType Type of Message
35 | */
36 | const logErrorOrSuccess = (messageType, delta) => {
37 | // Object containing metadata related to error or success
38 | let logObject = {
39 | // type : {error, success}
40 | // message : {Message} If type is 'error'
41 | // subType : {tab, group} If type is 'success'
42 | // url : {url_link} If subType is 'tab'
43 | // groupName: {Name of the Group} If subType is 'group'
44 | };
45 |
46 | // If logging enabled.
47 | if (saveTabsSettingsObject.logsState) {
48 | switch (messageType) {
49 | case `error`:
50 | logObject = {
51 | type: `error`,
52 | message: (delta.message || delta)
53 | };
54 | break;
55 | case `newTabCreated`:
56 | logObject = {
57 | type: `success`,
58 | subType: `tab`,
59 | url: (delta.status === "loading" ? delta.pendingUrl : delta.title)
60 | };
61 | break;
62 | case `newGroupCreated`:
63 | logObject = {
64 | type: `success`,
65 | subType: `group`,
66 | groupName: delta.title
67 | };
68 | break;
69 | }
70 |
71 | LoggerQueue.unshift({
72 | time: new Date().valueOf(),
73 | data: logObject
74 | });
75 | }
76 | };
77 |
78 | /**
79 | * Functionality to parse and create tabs and groups from recevied list of metadata of tabs and groups.
80 | * @version 1.0.0
81 | * @param {Object} fileContent Contains list of meta data of tabs and groups (if any).
82 | */
83 | const importTabs = (fileContent) => {
84 | // Extract list of tabs
85 | const listOfTabs = fileContent.tabs;
86 |
87 | // Group Tabs Queue to keep track of completion of tabGroups promise.
88 | const groupTabsQueue = new Map();
89 |
90 | // Array of Promises requested.
91 | const groupedTabsPromiseArray = [];
92 |
93 | for (const tab of listOfTabs) {
94 | if (browserDetected == 1 || tab.groupId === undefined || tab.groupId == -1) {
95 | // 1. If tabs are opened in FireFox
96 | // 2. For Tabs which are not grouped.
97 |
98 | // Create new tab and log outcome.
99 | browserAPI.tabs
100 | .create({
101 | url: tab.url,
102 | active: false
103 | })
104 | .then(
105 | logErrorOrSuccess.bind(null, `newTabCreated`),
106 | logErrorOrSuccess.bind(null, `error`)
107 | );
108 | } else {
109 | // Check if URL to be grouped.
110 | if (groupTabsQueue.has(tab.url)) {
111 | const getGroup = groupTabsQueue.get(tab.url);
112 |
113 | getGroup.set(
114 | tab.groupId,
115 | getGroup.has(tab.groupId) ? getGroup.get(tab.groupId) + 1 : 1
116 | );
117 |
118 | groupTabsQueue.set(tab.url, getGroup);
119 | } else {
120 | groupTabsQueue.set(tab.url, new Map([[tab.groupId, 1]]));
121 | }
122 |
123 | groupedTabsPromiseArray.push(
124 | // Create new tab
125 | browserAPI.tabs.create({
126 | url: tab.url,
127 | active: false,
128 | })
129 | );
130 | }
131 | }
132 |
133 | /**
134 | * Store all logs from LoggerQueue to Local Storage.
135 | * @version 1.0.0
136 | */
137 | const insertNewLogs = () => {
138 | // If logging enabled.
139 | if (saveTabsSettingsObject.logsState) {
140 | // Check if 'saveTabs' in present or not.
141 | browserAPI.storage.local.get(`saveTabs`, (object) => {
142 |
143 | setTimeout(() => {
144 | let FinalLogsQueue = [];
145 |
146 | // Latest logged message time.
147 | const updated_at = LoggerQueue[0].time;
148 |
149 | if (
150 | object &&
151 | Object.keys(object).length === 0 &&
152 | object.constructor === Object
153 | ) {
154 | FinalLogsQueue = LoggerQueue;
155 | } else {
156 | object.saveTabs.logs.unshift(...LoggerQueue);
157 | FinalLogsQueue = object.saveTabs.logs;
158 | }
159 |
160 | // Store logs to local storage and then empty LoggerQueue.
161 | browserAPI.storage.local.set(
162 | {
163 | saveTabs: {
164 | logs: FinalLogsQueue,
165 | updated_at: updated_at,
166 | },
167 | },
168 | () => {
169 | LoggerQueue = [];
170 | }
171 | );
172 | }, 1000);
173 | });
174 | }
175 | };
176 |
177 | /**
178 | * Store all logs from LoggerQueue to Local Storage.
179 | * @version 1.0.0
180 | * @param {Object} metaData Meta data of groups with list of respective tabs to be grouped.
181 | */
182 | const moveTabsToGroups = (metaData) => {
183 | metaData.forEach((group) => {
184 | // Create new Group with tabs listed.
185 | browserAPI.tabs.group({
186 | tabIds: group.tabs
187 | })
188 | .then((delta) => {
189 | browserAPI.tabGroups
190 | .update(delta, {
191 | title: group.title,
192 | collapsed: true,
193 | })
194 | .then(
195 | logErrorOrSuccess.bind(null, `newGroupCreated`),
196 | logErrorOrSuccess.bind(null, `error`)
197 | );
198 | }, logErrorOrSuccess.bind(null, `error`));
199 | });
200 |
201 | // To store all logs in local storage.
202 | insertNewLogs();
203 | };
204 |
205 | if (browserDetected == 1 || fileContent.groups === undefined) {
206 | // 1. If FireFox.
207 | // 2. If no grouped tabs are present.
208 |
209 | // To store all logs in local storage.
210 | insertNewLogs();
211 | } else {
212 | // Meta Data of newly created tabs to be grouped.
213 | const groupMetaData = new Map();
214 |
215 | // To check if all promised are fulfilled.
216 | Promise.allSettled(groupedTabsPromiseArray).then((results) => {
217 | results.forEach((result) => {
218 | // Get URL
219 | const url = result.value.url || result.value.pendingUrl;
220 |
221 | // URL to be grouped in.
222 | const groupsRelatedToURL = groupTabsQueue.get(url);
223 |
224 | // Group Id
225 | const groupIdForTab = groupsRelatedToURL.keys().next().value;
226 |
227 | const groupIdCount = groupsRelatedToURL.get(groupIdForTab);
228 |
229 | if (groupIdCount == 1) {
230 | ((groupsRelatedToURL.size == 1) ?
231 | groupTabsQueue.delete(url) :
232 | groupsRelatedToURL.delete(groupIdForTab));
233 | } else {
234 | groupsRelatedToURL.set(groupIdForTab, groupIdCount - 1);
235 | groupTabsQueue.set(url, groupsRelatedToURL);
236 | }
237 |
238 | const metaData = {
239 | title: fileContent.groups[groupIdForTab].title,
240 | };
241 |
242 | if (!groupMetaData.has(groupIdForTab)) {
243 | metaData[`tabs`] = [result.value.id];
244 | } else {
245 | metaData[`tabs`] = groupMetaData.get(groupIdForTab).tabs;
246 | metaData.tabs.push(result.value.id);
247 | }
248 |
249 | // Set meta data for respective Group.
250 | groupMetaData.set(groupIdForTab, metaData);
251 |
252 | // Log sucessful creation of tab.
253 | logErrorOrSuccess(`newTabCreated`, result.value);
254 |
255 | // If all promises are fulfilled then group tabs.
256 | if (groupTabsQueue.size == 0) {
257 | moveTabsToGroups(groupMetaData);
258 | }
259 | });
260 | }, logErrorOrSuccess.bind(null, `error`));
261 | }
262 | };
263 |
264 | /**
265 | * Interpret message received.
266 | * @version 1.0.0
267 | * @param {Object} message Message Object containing request and data.
268 | * @param {Object} sendResponse Function to respond recevier with parameters as Message Object.
269 | */
270 | const interpretRequest = (message, sender, sendResponse) => {
271 | // Check if requesting for import tabs functionality
272 | if (message.type === `imported_file_content`) {
273 | LoggerQueue = [];
274 | saveTabsSettingsObject = message.saveTabsSettings;
275 | importTabs(message.data);
276 | sendResponse(`Request Submitted Successfully!`);
277 | }
278 | };
279 |
280 | // Sync updated local storage changes
281 | browserAPI.storage.onChanged.addListener((changes, area) => {
282 | if (area === `local` && changes.saveTabsSettings !== undefined) {
283 | browserAPI.storage.local.get(`saveTabsSettings`, (object) => {
284 | saveTabsSettingsObject = object.saveTabsSettings;
285 | });
286 | }
287 | });
288 |
289 | // Listener to listen message received from saveTabs.js
290 | browserAPI.runtime.onMessage.addListener(interpretRequest);
291 |
292 | if (browserDetected == 1)
293 | try {
294 | // To toggle sidebar of Firefox browser
295 | browserAPI.browserAction.onClicked.addListener(() => {
296 | browserAPI.sidebarAction.toggle();
297 | });
298 | } catch (error) {
299 | console.log(error);
300 | }
301 |
--------------------------------------------------------------------------------
/extension/saveTab.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Save Tabs
8 |
9 |
10 |
11 |
12 |
13 |
14 |
19 |
20 |
25 |
37 |
38 |
43 |
55 |
56 |
57 |
69 |
70 |
71 |
72 |
73 |
262 |
270 |
271 |
272 |
273 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Save Tabs
9 |
10 |
16 |
22 |
23 |
24 |
25 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
![Save Tabs Logo]()
66 |
67 |
68 |
69 |
Save Tabs
70 |
71 |
72 | Extension to Export and Import Tabs.
73 |
74 |
75 |
81 |
82 |
Available On
83 |
84 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
Features
117 |
118 |
119 | -
120 | Easy Export and Import of Tabs with one click.
121 |
122 | - Save Exported Tabs with Custom Name.
123 | -
124 | Cross Browser Support. (as of now
125 | Chrome and Firefox)
126 |
127 | - Logs Section.
128 | -
129 | Support for Grouped Tabs (Chrome only).
130 |
131 |
132 |
133 |
134 |
135 |
Source Code
136 |
137 |
165 |
166 |
167 |
168 |
169 |
170 |
Guide
171 |
172 |
173 |
174 |
175 |
Export
176 |
177 |
178 |

182 |
183 |
184 |
185 | * Assuming multiple tabs are open in
187 | browser.
189 |
190 |
191 | # Default Export
192 |
193 |
194 | - Open Save Tabs Extension.
195 | - Click on Export button.
196 | - Native File Explorer open to save file.
197 | - Click on Save.
198 |
199 |
200 | # Custom Name Export
201 |
202 |
203 | - Open Save Tabs Extension.
204 | -
205 | Enter custom name in
206 | "Enter file name". (Ex.
207 | Research-Project-X)
208 |
209 | - Click on Export button.
210 | - Native File Explorer open to save file.
211 | - Click on Save.
212 |
213 |
Tabs will be exported successfully in file.
214 |
215 |
216 |
217 |
218 |
Import
219 |
220 |
221 |

225 |
226 |
227 |
228 | * Importing file which are exported using
230 | "Save Tabs" Extension.
232 |
233 |
234 | - Open Save Tabs Extension.
235 | - Click on Import button.
236 | -
237 | Native File Explorer opens. Select the file
238 | to be imported. (Ex.
239 | Research-Project-X.json)
240 |
241 |
242 |
Tabs will be imported successfully in
244 | browser.
246 |
247 |
248 |
249 |
250 |
Logs
251 |
252 |
253 |

257 |
258 |
259 |
260 | Logs Section lets user know the status of
261 | Export or Import tabs.
263 |
264 | # Enable/Disable Logs
265 |
266 |
267 | - Open Save Tabs Extension.
268 | -
269 | Click on toggle button next to
270 | Logs title.
271 |
272 |
273 |
274 | If Logs are enabled, user can see the
275 | status of Export or Import.
276 |
277 |
278 |
279 |
280 | * Logs will be displayed for 10 minutes from
281 | last use.
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
Gallery
291 |
292 |
293 |
294 |
295 |
298 |
299 |

303 |
304 |
305 | Preview of Save Tabs extension
306 |
307 |
308 |
309 |
310 |
313 |
314 |

318 |
319 |
320 | Exporting tabs
321 |
322 |
323 |
324 |
325 |
328 |
329 |

333 |
334 |
335 | Custom file name to store exported tabs
336 |
337 |
338 |
339 |
340 |
343 |
344 |

348 |
349 |
350 | Importing tabs
351 |
352 |
353 |
354 |
355 |
358 |
359 |

363 |
364 |
365 | Importing Grouped tabs
366 |
367 |
368 |
369 |
370 |
373 |
374 |

378 |
379 |
380 | Logging Section
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
Privacy
390 |
391 |
392 |
393 | NO DATA is COLLECTED or
394 | USED or SHARED .
395 |
396 |
397 |
398 |
399 |
400 |
414 |
415 |
416 |
417 |
--------------------------------------------------------------------------------
/extension/saveTab.css:
--------------------------------------------------------------------------------
1 | body,
2 | html {
3 | margin: 0;
4 | padding: 0;
5 | height: 100%;
6 | width: 100%;
7 | }
8 |
9 | :root {
10 | --color_0: white;
11 | --color_A: rgb(255, 0, 0);
12 | --color_A_light_1: rgb(255, 235, 235, 0.05);
13 | --color_A_light_1_1: rgb(255, 235, 235, 0.5);
14 | --color_A_light_2: rgb(255, 118, 118);
15 | --color_B: rgb(0, 128, 0);
16 | --color_B_light_1: rgb(229, 255, 229, 0.05);
17 | --color_B_light_2: rgb(0, 162, 0);
18 | --color_C: rgb(0, 0, 255);
19 | --color_C_light_1: rgb(229, 229, 255, 0.05);
20 | --color_C_light_2: rgb(179, 179, 255);
21 | --color_shadow: rgb(43, 122, 120);
22 | --color_error_interrupted: rgb(204, 0, 0);
23 | --color_success_complete: rgb(0, 128, 0);
24 | --color_ready: rgb(255, 165, 0);
25 | }
26 |
27 | body {
28 | display: -webkit-box;
29 | display: -ms-flexbox;
30 | display: flex;
31 | -webkit-box-orient: vertical;
32 | -webkit-box-direction: normal;
33 | -ms-flex-flow: column;
34 | flex-flow: column;
35 | -webkit-box-align: center;
36 | -ms-flex-align: center;
37 | align-items: center;
38 | }
39 |
40 | /**
41 | Navigation Bar
42 | */
43 | .navbar {
44 | display: -webkit-box;
45 | display: -ms-flexbox;
46 | display: flex;
47 | -webkit-box-orient: vertical;
48 | -webkit-box-direction: normal;
49 | -ms-flex-flow: column wrap;
50 | flex-flow: column wrap;
51 | padding: 0.5rem 0.5rem;
52 | width: 100%;
53 | height: auto;
54 | -webkit-box-align: center;
55 | -ms-flex-align: center;
56 | align-items: center;
57 | -ms-flex-line-pack: center;
58 | align-content: center;
59 | -webkit-box-pack: center;
60 | -ms-flex-pack: center;
61 | justify-content: center;
62 | }
63 |
64 | .navbar .navbar-content {
65 | display: -webkit-box;
66 | display: -ms-flexbox;
67 | display: flex;
68 | -webkit-box-orient: horizontal;
69 | -webkit-box-direction: normal;
70 | -ms-flex-flow: row wrap;
71 | flex-flow: row wrap;
72 | padding: 0.5rem 1rem;
73 | width: 100%;
74 | height: auto;
75 | -webkit-box-align: center;
76 | -ms-flex-align: center;
77 | align-items: center;
78 | -webkit-box-pack: justify;
79 | -ms-flex-pack: justify;
80 | justify-content: space-between;
81 | background-color: var(--color_0);
82 | border: 0.1rem solid;
83 | border-color: var(--color_A) var(--color_B) var(--color_C);
84 | border-radius: 0.5rem;
85 | }
86 |
87 | /* Extension Name and Logo */
88 | .navbar-content .header {
89 | display: -webkit-box;
90 | display: -ms-flexbox;
91 | display: flex;
92 | width: -webkit-max-content;
93 | width: -moz-max-content;
94 | width: max-content;
95 | -webkit-box-align: center;
96 | -ms-flex-align: center;
97 | align-items: center;
98 | }
99 |
100 | .header span {
101 | padding-left: 0.5rem;
102 | font-size: 2rem;
103 | font-weight: bold;
104 | font-variant-caps: petite-caps;
105 | }
106 |
107 | /* Extension Version */
108 | .header span#version {
109 | font-size: smaller;
110 | font-weight: 600;
111 | -ms-flex-item-align: end;
112 | align-self: flex-end;
113 | }
114 |
115 | /* Extension Support Links */
116 | .navbar-content .links {
117 | display: -webkit-box;
118 | display: -ms-flexbox;
119 | display: flex;
120 | -webkit-box-orient: horizontal;
121 | -webkit-box-direction: normal;
122 | -ms-flex-direction: row;
123 | flex-direction: row;
124 | -webkit-box-pack: justify;
125 | -ms-flex-pack: justify;
126 | justify-content: space-between;
127 | width: 9rem;
128 | }
129 |
130 | .links span {
131 | display: -webkit-box;
132 | display: -ms-flexbox;
133 | display: flex;
134 | -webkit-box-pack: center;
135 | -ms-flex-pack: center;
136 | justify-content: center;
137 | border-radius: 50%;
138 | -webkit-transition: 0.5s;
139 | -o-transition: 0.5s;
140 | transition: 0.5s;
141 | }
142 |
143 | .links span svg {
144 | width: 2.5rem;
145 | height: 2.5rem;
146 | }
147 |
148 | .links span:hover {
149 | background-color: var(--color_0);
150 | -webkit-filter: invert(1);
151 | filter: invert(1);
152 | cursor: pointer;
153 | }
154 |
155 | /**
156 | Content
157 | */
158 | .content {
159 | display: -webkit-box;
160 | display: -ms-flexbox;
161 | display: flex;
162 | -webkit-box-orient: vertical;
163 | -webkit-box-direction: normal;
164 | -ms-flex-direction: column;
165 | flex-direction: column;
166 | width: 100%;
167 | height: 100%;
168 | -webkit-box-align: center;
169 | -ms-flex-align: center;
170 | align-items: center;
171 | -webkit-box-pack: start;
172 | -ms-flex-pack: start;
173 | justify-content: flex-start;
174 | overflow: auto;
175 | }
176 |
177 | .export-section,
178 | .import-section,
179 | .logs-section {
180 | display: -webkit-box;
181 | display: -ms-flexbox;
182 | display: flex;
183 | -webkit-box-orient: vertical;
184 | -webkit-box-direction: normal;
185 | -ms-flex-direction: column;
186 | flex-direction: column;
187 | padding: 0.5rem;
188 | width: calc(100% - 1rem);
189 | height: calc(100% - 1rem);
190 | -webkit-box-pack: center;
191 | -ms-flex-pack: center;
192 | justify-content: center;
193 | }
194 |
195 | .export-section {
196 | height: 25%;
197 | }
198 | .import-section {
199 | height: 17%;
200 | }
201 |
202 | .logs-section {
203 | height: 58%;
204 | }
205 |
206 | .export-section fieldset {
207 | background-color: var(--color_A_light_1);
208 | border: 0.1rem solid var(--color_A);
209 | }
210 |
211 | .import-section fieldset {
212 | background-color: var(--color_B_light_1);
213 | border: 0.1rem solid var(--color_B);
214 | }
215 |
216 | .logs-section fieldset {
217 | background-color: var(--color_C_light_1);
218 | border: 0.1rem solid var(--color_C);
219 | }
220 |
221 | /* Fieldset */
222 | fieldset {
223 | display: -webkit-box;
224 | display: -ms-flexbox;
225 | display: flex;
226 | padding: 0.5rem;
227 | height: 100%;
228 | -webkit-box-orient: vertical;
229 | -webkit-box-direction: normal;
230 | -ms-flex-direction: column;
231 | flex-direction: column;
232 | -webkit-box-align: center;
233 | -ms-flex-align: center;
234 | align-items: center;
235 | -webkit-box-pack: center;
236 | -ms-flex-pack: center;
237 | justify-content: center;
238 | border-radius: 0.5rem;
239 | }
240 |
241 | /* Legend */
242 | legend {
243 | display: -webkit-box;
244 | display: -ms-flexbox;
245 | display: flex;
246 | -webkit-box-orient: horizontal;
247 | -webkit-box-direction: normal;
248 | -ms-flex-flow: row wrap;
249 | flex-flow: row wrap;
250 | margin-left: 0.5rem;
251 | padding: 0;
252 | width: calc(100% - 1rem);
253 | justify-items: center;
254 | -webkit-box-pack: justify;
255 | -ms-flex-pack: justify;
256 | justify-content: space-between;
257 | border-radius: 0.5rem;
258 | }
259 |
260 | /* Legend (Left) */
261 | legend .legend-left {
262 | display: -webkit-box;
263 | display: -ms-flexbox;
264 | display: flex;
265 | -webkit-box-orient: horizontal;
266 | -webkit-box-direction: normal;
267 | -ms-flex-flow: row wrap;
268 | flex-flow: row wrap;
269 | }
270 |
271 | .legend-left .legend-title,
272 | .legend-left .legend-element {
273 | display: -webkit-box;
274 | display: -ms-flexbox;
275 | display: flex;
276 | padding: 0 0.5rem;
277 | -webkit-box-align: center;
278 | -ms-flex-align: center;
279 | align-items: center;
280 | font-weight: 600;
281 | font-size: 1.75rem;
282 | font-variant-caps: petite-caps;
283 | background-color: var(--color_0);
284 | border-radius: 0.5rem;
285 | }
286 |
287 | .export-section .legend-title {
288 | color: var(--color_A);
289 | border: 0.1rem solid var(--color_A);
290 | }
291 |
292 | .import-section .legend-title {
293 | color: var(--color_B);
294 | border: 0.1rem solid var(--color_B);
295 | }
296 |
297 | .logs-section .legend-title {
298 | color: var(--color_C);
299 | border: 0.1rem solid var(--color_C);
300 | }
301 |
302 | .legend-left .legend-element {
303 | margin-left: 1rem;
304 | }
305 |
306 | /* Legend (Right) */
307 | legend .legend-right {
308 | display: -webkit-box;
309 | display: -ms-flexbox;
310 | display: flex;
311 | -webkit-box-orient: horizontal;
312 | -webkit-box-direction: normal;
313 | -ms-flex-direction: row;
314 | flex-direction: row;
315 | -webkit-box-align: center;
316 | -ms-flex-align: center;
317 | align-items: center;
318 | }
319 |
320 | .legend-right .info {
321 | display: -webkit-box;
322 | display: -ms-flexbox;
323 | display: flex;
324 | -webkit-box-orient: horizontal;
325 | -webkit-box-direction: normal;
326 | -ms-flex-direction: row;
327 | flex-direction: row;
328 | cursor: help;
329 | }
330 |
331 | .info .section-info {
332 | display: -webkit-box;
333 | display: -ms-flexbox;
334 | display: flex;
335 | width: 2rem;
336 | height: 2rem;
337 | -webkit-box-align: center;
338 | -ms-flex-align: center;
339 | align-items: center;
340 | -webkit-box-pack: center;
341 | -ms-flex-pack: center;
342 | justify-content: center;
343 | border-radius: 50%;
344 | background-color: var(--color_0);
345 | -webkit-transition: all 0.5s;
346 | -o-transition: all 0.5s;
347 | transition: all 0.5s;
348 | }
349 |
350 | .export-section .section-info:hover {
351 | background-color: var(--color_A);
352 | }
353 |
354 | .import-section .section-info:hover {
355 | background-color: var(--color_B);
356 | }
357 |
358 | .logs-section .section-info:hover {
359 | background-color: var(--color_C);
360 | }
361 |
362 | .section-info svg {
363 | width: 2rem;
364 | height: 2rem;
365 | -webkit-transition: all 0.5s;
366 | -o-transition: all 0.5s;
367 | transition: all 0.5s;
368 | }
369 |
370 | .export-section .section-info svg {
371 | fill: var(--color_A);
372 | }
373 |
374 | .import-section .section-info svg {
375 | fill: var(--color_B);
376 | }
377 |
378 | .logs-section .section-info svg {
379 | fill: var(--color_C);
380 | }
381 |
382 | .section-info:hover svg {
383 | fill: var(--color_0);
384 | }
385 |
386 | /* Tooltip */
387 | .info .tooltip {
388 | position: relative;
389 | display: -webkit-box;
390 | display: -ms-flexbox;
391 | display: flex;
392 | }
393 |
394 | .tooltip span {
395 | visibility: hidden;
396 | z-index: 2;
397 | position: absolute;
398 | right: 135%;
399 | padding: 0.25rem 0.5rem;
400 | width: -webkit-max-content;
401 | width: -moz-max-content;
402 | width: max-content;
403 | color: var(--color_0);
404 | text-align: center;
405 | border-radius: 0.5rem;
406 | opacity: 0;
407 | -webkit-transition: opacity 0.5s;
408 | -o-transition: opacity 0.5s;
409 | transition: opacity 0.5s;
410 | }
411 |
412 | .tooltip span:after {
413 | content: "";
414 | position: absolute;
415 | top: 50%;
416 | left: 100%;
417 | margin-top: -0.75rem;
418 | border-width: 0.75rem;
419 | border-style: solid;
420 | }
421 |
422 | .tooltip:hover span {
423 | visibility: visible;
424 | opacity: 1;
425 | }
426 |
427 | .export-section .info span {
428 | background-color: var(--color_A);
429 | }
430 |
431 | .export-section .info span:after {
432 | border-color: transparent transparent transparent var(--color_A);
433 | }
434 |
435 | .import-section .info span {
436 | background-color: var(--color_B);
437 | }
438 |
439 | .import-section .info span:after {
440 | border-color: transparent transparent transparent var(--color_B);
441 | }
442 |
443 | .logs-section .info span {
444 | background-color: var(--color_C);
445 | }
446 |
447 | .logs-section .info span:after {
448 | border-color: transparent transparent transparent var(--color_C);
449 | }
450 |
451 | /* Fielset content */
452 | .fieldset-content {
453 | display: -webkit-box;
454 | display: -ms-flexbox;
455 | display: flex;
456 | width: 100%;
457 | height: 100%;
458 | -webkit-box-orient: vertical;
459 | -webkit-box-direction: normal;
460 | -ms-flex-direction: column;
461 | flex-direction: column;
462 | -webkit-box-align: center;
463 | -ms-flex-align: center;
464 | align-items: center;
465 | -webkit-box-pack: space-evenly;
466 | -ms-flex-pack: space-evenly;
467 | justify-content: space-evenly;
468 | }
469 |
470 | .fieldset-content input#file-name {
471 | display: -webkit-box;
472 | display: -ms-flexbox;
473 | display: flex;
474 | padding: 0.5rem;
475 | height: 3rem;
476 | width: calc(100% - 2rem);
477 | -webkit-box-align: center;
478 | -ms-flex-align: center;
479 | align-items: center;
480 | border-radius: 0.5rem;
481 | border: none;
482 | border-bottom: 0.2rem solid var(--color_A_light_2);
483 | }
484 |
485 | input#file-name:focus {
486 | outline: none;
487 | background-color: var(--color_A_light_1_1);
488 | border-bottom: 0.2rem solid var(--color_A);
489 | }
490 |
491 | /* Import & Export Button */
492 | .button {
493 | display: -webkit-box;
494 | display: -ms-flexbox;
495 | display: flex;
496 | -webkit-box-orient: horizontal;
497 | -webkit-box-direction: normal;
498 | -ms-flex-flow: row wrap;
499 | flex-flow: row wrap;
500 | width: 10rem;
501 | height: 3rem;
502 | -webkit-box-align: center;
503 | -ms-flex-align: center;
504 | align-items: center;
505 | -webkit-box-pack: space-evenly;
506 | -ms-flex-pack: space-evenly;
507 | justify-content: space-evenly;
508 | font-size: 1.2rem;
509 | font-weight: 700;
510 | border-radius: 0.5rem;
511 | background-color: var(--color_0);
512 | cursor: pointer;
513 | }
514 |
515 | .button:hover {
516 | color: var(--color_0);
517 | }
518 |
519 | .button:hover svg {
520 | fill: var(--color_0);
521 | }
522 |
523 | .button,
524 | .button svg {
525 | -webkit-transition: all 0.5s ease-in-out;
526 | -o-transition: all 0.5s ease-in-out;
527 | transition: all 0.5s ease-in-out;
528 | }
529 |
530 | .button span {
531 | width: 2rem;
532 | height: 2rem;
533 | }
534 |
535 | .export svg {
536 | fill: var(--color_A);
537 | }
538 |
539 | .import svg {
540 | fill: var(--color_B);
541 | }
542 |
543 | .export {
544 | color: var(--color_A);
545 | border: 0.2rem solid var(--color_A);
546 | -webkit-box-shadow: 0 0.25rem 0.25rem var(--color_A_light_2);
547 | box-shadow: 0 0.25rem 0.25rem var(--color_A_light_2);
548 | }
549 |
550 | .export:hover {
551 | background-color: var(--color_A);
552 | -webkit-box-shadow: 0 0.5rem 0.25rem var(--color_A_light_2);
553 | box-shadow: 0 0.5rem 0.25rem var(--color_A_light_2);
554 | }
555 |
556 | .export:active {
557 | -webkit-box-shadow: 0 0 0.25rem var(--color_A_light_2);
558 | box-shadow: 0 0 0.25rem var(--color_A_light_2);
559 | }
560 |
561 | .import {
562 | color: var(--color_B);
563 | border: 0.2rem solid var(--color_B);
564 | -webkit-box-shadow: 0 0.25rem 0.25rem var(--color_B_light_2);
565 | box-shadow: 0 0.25rem 0.25rem var(--color_B_light_2);
566 | }
567 |
568 | .import:hover {
569 | background-color: var(--color_B);
570 | -webkit-box-shadow: 0 0.5rem 0.25rem var(--color_B_light_2);
571 | box-shadow: 0 0.5rem 0.25rem var(--color_B_light_2);
572 | }
573 |
574 | .import:active {
575 | -webkit-box-shadow: 0 0 0.25rem var(--color_B_light_2);
576 | box-shadow: 0 0 0.25rem var(--color_B_light_2);
577 | }
578 |
579 | /* Logs Section */
580 | .logs-section .fieldset-content {
581 | -webkit-box-pack: start;
582 | -ms-flex-pack: start;
583 | justify-content: flex-start;
584 | }
585 |
586 | .fieldset-content .logs {
587 | display: -webkit-box;
588 | display: -ms-flexbox;
589 | display: flex;
590 | -webkit-box-orient: vertical;
591 | -webkit-box-direction: normal;
592 | -ms-flex-direction: column;
593 | flex-direction: column;
594 | padding: 0.5rem;
595 | width: 100%;
596 | -webkit-box-pack: start;
597 | -ms-flex-pack: start;
598 | justify-content: flex-start;
599 | font-size: 1.1rem;
600 | overflow-y: auto;
601 | scrollbar-width: thin;
602 | scrollbar-color: var(--color_C) var(--color_C_light_2);
603 | }
604 |
605 | .logs .log {
606 | display: -webkit-box;
607 | display: -ms-flexbox;
608 | display: flex;
609 | -webkit-box-orient: vertical;
610 | -webkit-box-direction: normal;
611 | -ms-flex-direction: column;
612 | flex-direction: column;
613 | max-width: 31rem;
614 | padding: 0.5rem;
615 | }
616 |
617 | .log .meta-data {
618 | display: -webkit-box;
619 | display: -ms-flexbox;
620 | display: flex;
621 | -webkit-box-orient: horizontal;
622 | -webkit-box-direction: normal;
623 | -ms-flex-direction: row;
624 | flex-direction: row;
625 | padding: 0.25rem 0;
626 | -webkit-box-align: center;
627 | -ms-flex-align: center;
628 | align-items: center;
629 | }
630 |
631 | .meta-data .timestamp {
632 | display: -webkit-box;
633 | display: -ms-flexbox;
634 | display: flex;
635 | width: 7rem;
636 | -webkit-box-align: center;
637 | -ms-flex-align: center;
638 | align-items: center;
639 | -webkit-box-pack: left;
640 | -ms-flex-pack: left;
641 | justify-content: left;
642 | font-weight: bold;
643 | }
644 |
645 | .meta-data .status {
646 | display: -webkit-box;
647 | display: -ms-flexbox;
648 | display: flex;
649 | margin-left: 0.5rem;
650 | padding: 0 0.5rem;
651 | font-weight: bold;
652 | border-radius: 0.25rem;
653 | color: var(--color_0);
654 | }
655 |
656 | .error,
657 | .interrupted {
658 | background-color: var(--color_error_interrupted);
659 | }
660 |
661 | .success,
662 | .complete {
663 | background-color: var(--color_success_complete);
664 | }
665 |
666 | .ready {
667 | background-color: var(--color_ready);
668 | }
669 |
670 | .log .description {
671 | max-width: calc(100% - 0.5rem);
672 | word-break: break-all;
673 | }
674 |
675 | .description .highlight {
676 | font-weight: bold;
677 | }
678 |
679 | /* Log Toggle Button */
680 | input[type="checkbox"] {
681 | height: 0;
682 | width: 0;
683 | display: none;
684 | }
685 |
686 | input[type="checkbox"] + label {
687 | display: -webkit-box;
688 | display: -ms-flexbox;
689 | display: flex;
690 | -webkit-box-sizing: border-box;
691 | box-sizing: border-box;
692 | width: 3.5rem;
693 | height: 2rem;
694 | border-radius: 2rem;
695 | background: var(--color_C_light_2);
696 | cursor: pointer;
697 | }
698 |
699 | input[type="checkbox"] + label:after {
700 | content: "";
701 | position: relative;
702 | top: 0.25rem;
703 | left: 0.25rem;
704 | width: 1.5rem;
705 | height: 1.5rem;
706 | background: var(--color_0);
707 | border-radius: 50%;
708 | -webkit-transition: 0.3s;
709 | -o-transition: 0.3s;
710 | transition: 0.3s;
711 | }
712 |
713 | input[type="checkbox"]:checked + label {
714 | background: var(--color_C);
715 | }
716 |
717 | input[type="checkbox"]:checked + label:after {
718 | left: calc(100% - 0.25rem);
719 | -webkit-transform: translateX(-100%);
720 | -ms-transform: translateX(-100%);
721 | transform: translateX(-100%);
722 | }
723 |
724 | input[type="checkbox"] + label:active:after {
725 | width: 2rem;
726 | }
727 |
728 | /**
729 | Footer
730 | */
731 | .footer {
732 | display: -webkit-box;
733 | display: -ms-flexbox;
734 | display: flex;
735 | -webkit-box-orient: vertical;
736 | -webkit-box-direction: normal;
737 | -ms-flex-flow: column wrap;
738 | flex-flow: column wrap;
739 | padding: 0.5rem 0.5rem;
740 | width: 100%;
741 | height: auto;
742 | -webkit-box-align: center;
743 | -ms-flex-align: center;
744 | align-items: center;
745 | -ms-flex-line-pack: center;
746 | align-content: center;
747 | -webkit-box-pack: center;
748 | -ms-flex-pack: center;
749 | justify-content: center;
750 | }
751 |
752 | .footer .footer-content {
753 | display: -webkit-box;
754 | display: -ms-flexbox;
755 | display: flex;
756 | -webkit-box-orient: horizontal;
757 | -webkit-box-direction: normal;
758 | -ms-flex-flow: row wrap;
759 | flex-flow: row wrap;
760 | padding: 0.5rem 1rem;
761 | width: 100%;
762 | height: auto;
763 | font-variant-caps: petite-caps;
764 | -webkit-box-align: center;
765 | -ms-flex-align: center;
766 | align-items: center;
767 | -webkit-box-pack: center;
768 | -ms-flex-pack: center;
769 | justify-content: center;
770 | background-color: var(--color_0);
771 | border: 0.1rem solid;
772 | border-color: var(--color_A) var(--color_B) var(--color_C);
773 | border-radius: 0.5rem;
774 | }
775 |
776 | .footer-content .developer-name {
777 | padding: 0 0.25rem;
778 | font-size: 1.2rem;
779 | font-weight: bold;
780 | }
781 |
782 | /**
783 | Extras
784 | */
785 | .hidden {
786 | display: none;
787 | }
788 |
789 | .box-shadow {
790 | -webkit-box-shadow: 0 0 0.3rem var(--color_shadow);
791 | box-shadow: 0 0 0.3rem var(--color_shadow);
792 | }
--------------------------------------------------------------------------------
/extension/saveTab.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @file Script for Exporting and Importing Tabs.
3 | * @author Vedant Wakalkar
4 | */
5 |
6 | `use strict`;
7 |
8 | window.addEventListener(`DOMContentLoaded`, () => {
9 | /**
10 | * Firefox 1
11 | * Google Chrome 2
12 | */
13 | let browserDetected = 1;
14 |
15 | // Browser's API Object.
16 | let browserAPI;
17 |
18 | try {
19 | // Firefox
20 | browserAPI = browser;
21 | } catch (error) {
22 | // Chrome
23 | browserDetected = 2;
24 | browserAPI = chrome;
25 | }
26 |
27 | // Save Tabs Settings Object
28 | let saveTabsSettingsObject = {
29 | // Disabled by default
30 | logsState: false
31 | };
32 |
33 | // Custom message for different type of errors or success.
34 | const logMessageMapping = {
35 | success: {
36 | tab: `Created new tab for `,
37 | group: `Tabs successfully grouped.`
38 | },
39 | ready: `Ready for download - `,
40 | interrupted: `Download interrupted - `,
41 | complete: `Download completed - `
42 | };
43 |
44 | /**
45 | * Set new settings in local storage.
46 | * @version 1.0.0
47 | * @param {String} type To determine to intialize or update settings in local storage. (`intialize` or `update`)
48 | */
49 | const saveTabsSettings = (type) => {
50 | // Check if 'saveTabsSettings' in present or not.
51 | browserAPI.storage.local.get(`saveTabsSettings`, (object) => {
52 | if (
53 | (object &&
54 | Object.keys(object).length === 0 &&
55 | object.constructor === Object) || (type === `update`)
56 | ) {
57 | // Store settings to local storage.
58 | browserAPI.storage.local.set({
59 | saveTabsSettings: saveTabsSettingsObject
60 | });
61 | } else if (type === `intialize`) {
62 | saveTabsSettingsObject = object.saveTabsSettings;
63 | document.getElementById(`logs-state`).checked = object.saveTabsSettings.logsState;
64 | updateDOMSettings();
65 | }
66 | });
67 | };
68 |
69 | /**
70 | * Update DOM elements based on saveTabsSettings
71 | * @version 1.0.0
72 | */
73 | const updateDOMSettings = () => {
74 | document.getElementById(`logs-state`).checked = saveTabsSettingsObject.logsState;
75 | };
76 |
77 | /**
78 | * Store logs in local storage.
79 | * @version 1.1.0
80 | * @param {Object} object Metadata related to success or error log.
81 | */
82 | const logger = (object) => {
83 | const logObject = {
84 | time: new Date().valueOf(),
85 | data: object
86 | };
87 |
88 | // Check if 'saveTabs' in present or not.
89 | browserAPI.storage.local.get(`saveTabs`, (object) => {
90 | // Latest logged message time.
91 | const updated_at = logObject.time;
92 | let FinalLogsQueue;
93 | if (
94 | object &&
95 | Object.keys(object).length === 0 &&
96 | object.constructor === Object
97 | ) {
98 | FinalLogsQueue = [logObject];
99 | } else {
100 | object.saveTabs.logs.unshift(logObject);
101 | FinalLogsQueue = object.saveTabs.logs;
102 | }
103 |
104 | // Store logs to local storage..
105 | browserAPI.storage.local.set({
106 | saveTabs: {
107 | logs: FinalLogsQueue,
108 | updated_at: updated_at
109 | }
110 | });
111 | });
112 | };
113 |
114 | /**
115 | * Populates Logs Section of Extension by logs stored in local storage.
116 | * @version 1.1.0
117 | */
118 | const populateLogs = () => {
119 | // DOM of log section.
120 | const logSection = document.getElementById(`logs`);
121 |
122 | // Clear all log section contents.
123 | // TODO: Redesigning logging mechanism and optimize populating logs.
124 | logSection.innerText = ``;
125 |
126 | // Get logs from local storage
127 | browserAPI.storage.local.get(`saveTabs`, (object) => {
128 | if (object.saveTabs !== undefined) {
129 | object.saveTabs.logs
130 | .map(
131 | (log) => {
132 | const data = log.data;
133 | // Log message in detail
134 | let verboseMessage = {
135 | normal: '',
136 | highlighted: ''
137 | };
138 |
139 | if (data.type === `error`) {
140 | // If type is 'error'
141 | verboseMessage.normal = data.message;
142 | } else if (data.type === `success`) {
143 | // If type is 'success'
144 | verboseMessage.normal = logMessageMapping[data.type][data.subType];
145 |
146 | if (data.subType === `tab`)
147 | // If sub-type is 'tab'
148 | verboseMessage.highlighted = data.url;
149 | else
150 | // If sub-type is 'group'
151 | if (data.groupName !== ``)
152 | verboseMessage.highlighted = ` ( ${data.groupName} )`;
153 | } else {
154 | // If type is 'ready' or 'interrupted' or 'complete'
155 | verboseMessage.normal = logMessageMapping[data.type];
156 | verboseMessage.highlighted = data.fileName;
157 | }
158 |
159 | // Individual log division.
160 | let logDiv = document.createElement(`div`);
161 | logDiv.className = `log`;
162 |
163 | // Log's meta-data division.
164 | let metaDataDiv = document.createElement(`div`);
165 | metaDataDiv.className = 'meta-data';
166 |
167 | // Meta-data - timestamp span.
168 | let timeStampSpan = document.createElement(`span`);
169 | timeStampSpan.className = `timestamp`;
170 | timeStampSpan.appendChild(document.createTextNode(new Date(log.time).toLocaleTimeString().toUpperCase()));
171 | metaDataDiv.appendChild(timeStampSpan);
172 |
173 | // Meta-data - status span.
174 | let statusSpan = document.createElement(`span`);
175 | statusSpan.className = `status ${data.type}`;
176 | statusSpan.appendChild(document.createTextNode(data.type.toUpperCase()));
177 | metaDataDiv.appendChild(statusSpan);
178 |
179 | logDiv.appendChild(metaDataDiv);
180 |
181 | // Log's description division.
182 | let descriptionDiv = document.createElement(`div`);
183 | descriptionDiv.className = `description`;
184 | descriptionDiv.appendChild(document.createTextNode(verboseMessage.normal));
185 |
186 | if (verboseMessage.highlighted !== ``) {
187 | let highlightSpan = document.createElement('span');
188 | highlightSpan.className = 'highlight';
189 | highlightSpan.appendChild(document.createTextNode(verboseMessage.highlighted));
190 | descriptionDiv.appendChild(highlightSpan);
191 | }
192 |
193 | logDiv.appendChild(descriptionDiv);
194 |
195 | logSection.appendChild(logDiv);
196 | })
197 | .join(``);
198 | }
199 | });
200 | };
201 |
202 | /**
203 | * Logging error or success message.
204 | * @version 1.0.0
205 | * @param {Object} delta Object returned from Promise.
206 | * @param {String} messageType Type of Message
207 | */
208 | const logErrorOrSuccess = (messageType, delta) => {
209 | // Object containing metadata related to error or success
210 | let logObject = {
211 | // type : {error, ready, interrupted, complete}
212 | // message : {Message} If type is 'error'
213 | // fileName : {Name of the file} If subType is 'ready' or 'interrupted' or 'complete'
214 | };
215 |
216 | // If logging enabled.
217 | if (saveTabsSettingsObject.logsState) {
218 | switch (messageType) {
219 | case `error`:
220 | logObject = {
221 | type: `error`,
222 | message: (delta.message || delta)
223 | };
224 | break;
225 | case `readyForDownload`:
226 | logObject = {
227 | type: `ready`,
228 | fileName: delta.fileName
229 | };
230 | break;
231 | case `downloadedStatus`:
232 | logObject = {
233 | type: delta.state,
234 | fileName: delta.fileName
235 | };
236 | break;
237 | }
238 |
239 | logger(logObject);
240 | }
241 | };
242 |
243 | /**
244 | * Read the uploaded file and open corresponding URLs in new tabs.
245 | * @version 1.1.0
246 | * @param {Object} fileInput Event Object of File Input when a file is uploaded.
247 | */
248 | const importTabs = (fileInput) => {
249 | // Read the data from uploaded file.
250 | const file = fileInput.target.files[0];
251 |
252 | const reader = new FileReader();
253 | reader.readAsText(file, `UTF-8`);
254 | reader.onload = (e) => {
255 | const fileContent = JSON.parse(e.target.result);
256 |
257 | // Send Message to background.js to run creation of tabs any group.
258 | browserAPI.runtime.sendMessage(
259 | {
260 | type: `imported_file_content`,
261 | data: fileContent,
262 | saveTabsSettings: saveTabsSettingsObject
263 | },
264 | (response) => {
265 | try {
266 | console.log(response);
267 | // Close popup in Chrome. No affect on Firefox sidebar.
268 | // If popup remains, one of the observed issues is on importing tabs (all of them grouped) and then importing new file, it doesn't get import.
269 | // Need to check if this can be better handled by handling file reader in more efficient manner.
270 | window.close();
271 | } catch (error) {
272 | console.log(error);
273 | }
274 | }
275 | );
276 | };
277 | };
278 |
279 | /**
280 | * Returns default or user input file name.
281 | * Default File Name Format : "Save_Tabs_DD-MM-YYYY-hh-mm-ss-*M.json"
282 | * @version 1.0.0
283 | * @return {String} Name of the file.
284 | */
285 | const getFilename = () => {
286 | const filenameElement = document.getElementById(`file-name`);
287 |
288 | const fileName = filenameElement.value
289 | .trim()
290 | .replace(/[^\w\s_\(\)\-]/gi, ``);
291 |
292 | if (fileName == ``) {
293 | // Default File Name.
294 | return `Save_Tabs_${new Date()
295 | .toLocaleString()
296 | .replaceAll(/(, )| /g, "_")
297 | .replaceAll(/[,://]/g, "-").toUpperCase()}.json`;
298 | } else {
299 | // User Input File Name.
300 | return `${fileName}.json`;
301 | }
302 | };
303 |
304 | /**
305 | * Download file (in JSON format) containing list of all open tabs.
306 | * @version 1.1.0
307 | * @param {Object} tabs Detailed array of all open Tabs.
308 | */
309 | const downloadFile = (tabs) => {
310 | // Download Queue to keep track of each download event.
311 | const downloadQueue = new Map();
312 |
313 | // Group Tabs Promise Queue to keep track of completion of tabGroups promise.
314 | const groupTabsQueue = new Map();
315 |
316 | // Promise array to keep track of all promises requested.
317 | let promiseArray = [];
318 |
319 | // Structure of file which will be downloaded
320 | let fileContent = Object({
321 | tabs: []
322 | });
323 |
324 | /**
325 | * To check if file has completed download or not.
326 | * @version 1.0.0
327 | * @param {Object} delta Object of download intiated file.
328 | */
329 | const onChangedListener = (delta) => {
330 | if (
331 | downloadQueue.has(delta.id) &&
332 | delta.state &&
333 | delta.state.current !== `in_progress`
334 | ) {
335 | // Check if the download ID present in download queue. If present then check status.
336 | const mappedValueForID = downloadQueue.get(delta.id);
337 |
338 | // Delete entry for download ID from download queue.
339 | downloadQueue.delete(delta.id);
340 |
341 | // Revoke File URL created.
342 | window.URL.revokeObjectURL(mappedValueForID.url);
343 |
344 | logErrorOrSuccess(`downloadedStatus`, {
345 | state: delta.state.current,
346 | fileName: mappedValueForID.fileName
347 | });
348 |
349 | // Remove Listener
350 | browserAPI.downloads.onChanged.removeListener(onChangedListener);
351 | }
352 | };
353 |
354 | /**
355 | * Convert File Content Object to blob and downloads file.
356 | * @version 1.0.0
357 | * @param {Object} fileContent Object containing data related to tabs.
358 | */
359 | const initiateDownload = (fileContent) => {
360 | // Create Blob of file content
361 | const file = new Blob([JSON.stringify(fileContent)], {
362 | type: `plain/text`
363 | });
364 |
365 | // Get file name
366 | const fileName = getFilename();
367 |
368 | // Create URL of Blob file
369 | const url = window.URL.createObjectURL(file);
370 |
371 | const metaData = {
372 | url: url,
373 | filename: fileName,
374 | saveAs: true,
375 | conflictAction: `uniquify`
376 | };
377 |
378 | logErrorOrSuccess(`readyForDownload`, {
379 | fileName: fileName
380 | });
381 |
382 | if (browserDetected == 1) {
383 | // Firefox
384 | browserAPI.downloads.download(metaData).then(
385 | (id) => {
386 | downloadQueue.set(id, {
387 | url,
388 | fileName
389 | });
390 |
391 | // Add Listener to keep track of download
392 | browserAPI.downloads.onChanged.addListener(onChangedListener);
393 | }, () => {
394 | logErrorOrSuccess(`downloadedStatus`, {
395 | state: `interrupted`,
396 | fileName: fileName
397 | });
398 | }
399 | );
400 | } else {
401 | // Chrome
402 | browserAPI.downloads.download(metaData, (id) => {
403 | downloadQueue.set(id, {
404 | url,
405 | fileName
406 | });
407 |
408 | // Add Listener to keep track of download
409 | browserAPI.downloads.onChanged.addListener(onChangedListener);
410 | });
411 | }
412 | };
413 |
414 | // Restructuring tabs with required details (url and groupId)
415 | tabs.map((tab) => {
416 | if (tab.groupId === undefined || tab.groupId == -1) {
417 | fileContent.tabs.push({
418 | url: tab.url
419 | });
420 | } else {
421 | fileContent.tabs.push({
422 | url: tab.url,
423 | groupId: tab.groupId
424 | });
425 |
426 | if (tab.groupId != -1 && !groupTabsQueue.has(tab.groupId)) {
427 | groupTabsQueue.set(tab.groupId, 1);
428 | promiseArray.push(browserAPI.tabGroups.get(tab.groupId));
429 | }
430 | }
431 | });
432 |
433 | if (groupTabsQueue.size || promiseArray.length) {
434 | // if group details are requested
435 | Promise.allSettled(promiseArray).then((results) => {
436 | results.forEach((result) => {
437 | groupTabsQueue.delete(result.value.id);
438 |
439 | if (fileContent.groups === undefined) {
440 | fileContent[`groups`] = {};
441 | }
442 |
443 | fileContent.groups[result.value.id] = {
444 | title: result.value.title,
445 | };
446 |
447 | // If all promises are fulfilled then intiate download.
448 | if (groupTabsQueue.size == 0) {
449 | initiateDownload(fileContent);
450 | }
451 | });
452 | }, logErrorOrSuccess.bind(null, `error`));
453 | } else {
454 | // If no group details request initiated.
455 | initiateDownload(fileContent);
456 | }
457 | };
458 |
459 | /**
460 | * Query browser window to get list of all open tabs.
461 | * On successfull query, file is downloaded or error is logged on failure.
462 | * @version 1.0.0
463 | */
464 | const exportTabs = () => {
465 | // Get list all tabs open in current browser window
466 | browserAPI.tabs
467 | .query({
468 | currentWindow: true,
469 | })
470 | .then(downloadFile, logErrorOrSuccess.bind(null, `error`));
471 | };
472 |
473 | /**
474 | * Checks and deletes (older than 10 mins) data stored in localstorage
475 | * @version 1.0.0
476 | */
477 | const localStorageExpiryCheck = () => {
478 | // Check if 'saveTabs' in present or not.
479 | browserAPI.storage.local.get(`saveTabs`, (object) => {
480 | if (object.saveTabs !== undefined)
481 | (Date.now() - object.saveTabs.updated_at > 600000) ?
482 | browserAPI.storage.local.remove('saveTabs') :
483 | populateLogs();
484 | });
485 | };
486 |
487 | /**
488 | * Open URL in new tabs.
489 | * @version 1.0.0
490 | * @param {String} type To determine URL to be opened.
491 | */
492 | const openURL = (type) => {
493 | let url;
494 | switch (type) {
495 | case `links-github`:
496 | url = `https://github.com/karna98/Save-Tabs`;
497 | break;
498 | case `links-report`:
499 | url = `https://github.com/karna98/Save-Tabs#issues-and-suggestions`;
500 | break;
501 | case `links-guide`:
502 | url = `https://karna98.github.io/Save-Tabs/#guide`;
503 | break;
504 | }
505 |
506 | browserAPI.tabs
507 | .create({
508 | url: url
509 | });
510 | };
511 |
512 | /**
513 | * Intializes required checks and listeners.
514 | * @version 1.0.0
515 | */
516 | const init = () => {
517 | // Check and Clear (old than 10 minutes) data stored.
518 | localStorageExpiryCheck();
519 |
520 | // Get current version from manifest.json
521 | const currentVersion = browserAPI.runtime.getManifest().version;
522 |
523 | // DOM of version element.
524 | const versionDOMElement = document.getElementById(`version`);
525 | // Set title.
526 | versionDOMElement.title = `ver. ` + currentVersion;
527 | // Set innertext of DOM element.
528 | versionDOMElement.innerText = `ver. ` + currentVersion;
529 |
530 | // Detect if file is selected using HTML input file.
531 | document.getElementById(`file`).addEventListener(`change`, importTabs);
532 |
533 | // Listen on clicking Export button.
534 | document.getElementById(`export`).addEventListener(`click`, exportTabs);
535 |
536 | // Sync updated local storage changes.
537 | browserAPI.storage.onChanged.addListener((changes, area) => {
538 | if (area === `local` && changes.saveTabs !== undefined) {
539 | populateLogs();
540 | } else if (area === `local` && changes.saveTabsSettings !== undefined) {
541 | updateDOMSettings();
542 | }
543 | });
544 |
545 | // Check or Intialize saveTabs settings.
546 | saveTabsSettings(`intialize`);
547 |
548 | // Get updated state of logs state (enabled/disabled)
549 | document.getElementById(`logs-state`).addEventListener(`click`, (e) => {
550 | saveTabsSettingsObject.logsState = e.target.checked;
551 | saveTabsSettings(`update`);
552 | });
553 |
554 | // Open link in new tab when clicked.
555 | document.getElementById(`links-github`).addEventListener(`click`, () => openURL(`links-github`));
556 | document.getElementById(`links-report`).addEventListener(`click`, () => openURL(`links-report`));
557 | document.getElementById(`links-guide`).addEventListener(`click`, () => openURL(`links-guide`));
558 | };
559 |
560 | init();
561 | });
562 |
--------------------------------------------------------------------------------
/style.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | @font-face {
8 | font-family: B612;
9 | src: url(assets/B612/B612-Regular.ttf);
10 | }
11 |
12 | :root {
13 | --background-color: #f4f9f9;
14 | --color_0: white;
15 | --color_1: black;
16 | --color_A: rgb(255, 0, 0);
17 | --color_A_light_1: rgb(255, 235, 235, 0.05);
18 | --color_A_light_1_1: rgb(255, 235, 235, 0.5);
19 | --color_A_light_2: rgb(255, 118, 118);
20 | --color_B: rgb(0, 128, 0);
21 | --color_B_light_1: rgb(229, 255, 229, 0.05);
22 | --color_B_light_2: rgb(0, 162, 0);
23 | --color_C: rgb(0, 0, 255);
24 | --color_C_light_1: rgb(229, 229, 255, 0.05);
25 | --color_C_light_2: rgb(179, 179, 255);
26 | --color_shadow: rgb(43, 122, 120);
27 | --color_error_interrupted: rgb(204, 0, 0);
28 | --color_success_complete: rgb(0, 128, 0);
29 | --color_ready: rgb(255, 165, 0);
30 | }
31 |
32 | html {
33 | background-color: var(--background-color);
34 | font-family: B612;
35 | scroll-padding-top: 5rem;
36 | }
37 |
38 | * {
39 | scrollbar-width: thin;
40 | scrollbar-color: var(--color_shadow);
41 | }
42 |
43 | body {
44 | display: -webkit-box;
45 | display: -ms-flexbox;
46 | display: flex;
47 | -webkit-box-orient: vertical;
48 | -webkit-box-direction: normal;
49 | -ms-flex-flow: column;
50 | flex-flow: column;
51 | padding: 0 3rem;
52 | }
53 |
54 | h2,
55 | h3,
56 | h4 {
57 | display: -webkit-box;
58 | display: -ms-flexbox;
59 | display: flex;
60 | margin: 0;
61 | }
62 |
63 | h2 {
64 | font-size: 4rem;
65 | }
66 |
67 | h3 {
68 | font-size: 2.5rem;
69 | }
70 |
71 | h4 {
72 | font-size: 2rem;
73 | }
74 |
75 | header {
76 | display: -webkit-box;
77 | display: -ms-flexbox;
78 | display: flex;
79 | position: fixed;
80 | top: 0;
81 | left: 3rem;
82 | width: calc(100% - 6rem);
83 | height: 4rem;
84 | padding: 0.5rem 0;
85 | z-index: 1;
86 | background-color: var(--background-color);
87 | }
88 |
89 | nav,
90 | footer {
91 | display: -webkit-box;
92 | display: -ms-flexbox;
93 | display: flex;
94 | -webkit-box-orient: horizontal;
95 | -webkit-box-direction: normal;
96 | -ms-flex-direction: row;
97 | flex-direction: row;
98 | -webkit-box-pack: justify;
99 | -ms-flex-pack: justify;
100 | justify-content: space-between;
101 | -webkit-box-align: center;
102 | -ms-flex-align: center;
103 | align-items: center;
104 | padding: 0.25rem 0.5rem;
105 | background-color: var(--color_0);
106 | border: 0.1rem solid;
107 | border-color: var(--color_A) var(--color_B) var(--color_C);
108 | }
109 |
110 | nav {
111 | -webkit-box-flex: 1;
112 | -ms-flex: 1;
113 | flex: 1;
114 | }
115 |
116 | nav a {
117 | text-decoration: none;
118 | }
119 |
120 | nav .nav-left {
121 | display: -webkit-box;
122 | display: -ms-flexbox;
123 | display: flex;
124 | -webkit-box-pack: justify;
125 | -ms-flex-pack: justify;
126 | justify-content: space-between;
127 | }
128 |
129 | .nav-left .nav-logo {
130 | display: -webkit-box;
131 | display: -ms-flexbox;
132 | display: flex;
133 | -webkit-box-align: center;
134 | -ms-flex-align: center;
135 | align-items: center;
136 | }
137 |
138 | .nav-logo img {
139 | content: url("extension/icons/Save_Tabs_48.png");
140 | }
141 |
142 | .nav-logo span {
143 | padding: 0 0.5rem;
144 | font-size: 2rem;
145 | font-weight: 500;
146 | font-variant-caps: petite-caps;
147 | }
148 |
149 | nav ul {
150 | display: -webkit-box;
151 | display: -ms-flexbox;
152 | display: flex;
153 | padding: 0;
154 | height: calc(100% - 1.5rem);
155 | -webkit-box-pack: space-evenly;
156 | -ms-flex-pack: space-evenly;
157 | justify-content: space-evenly;
158 | -webkit-box-align: center;
159 | -ms-flex-align: center;
160 | align-items: center;
161 | }
162 |
163 | nav ul li {
164 | display: -webkit-box;
165 | display: -ms-flexbox;
166 | display: flex;
167 | list-style: none;
168 | -webkit-box-pack: center;
169 | -ms-flex-pack: center;
170 | justify-content: center;
171 | padding: 0.5rem 1rem;
172 | }
173 |
174 | nav ul li .nav-link {
175 | display: -webkit-box;
176 | display: -ms-flexbox;
177 | display: flex;
178 | width: 5rem;
179 | padding: 0.25rem;
180 | font-size: 1.5rem;
181 | font-weight: 400;
182 | font-variant-caps: small-caps;
183 | -webkit-box-pack: center;
184 | -ms-flex-pack: center;
185 | justify-content: center;
186 | }
187 |
188 | nav ul li .nav-link:hover {
189 | -webkit-box-shadow: 0 0.05rem 0.05rem var(--color_shadow);
190 | box-shadow: 0 0.05rem 0.05rem var(--color_shadow);
191 | }
192 |
193 | .nav-left .hamburger {
194 | display: none;
195 | }
196 |
197 | .hamburger .bars {
198 | display: -webkit-box;
199 | display: -ms-flexbox;
200 | display: flex;
201 | -webkit-box-orient: vertical;
202 | -webkit-box-direction: normal;
203 | -ms-flex-direction: column;
204 | flex-direction: column;
205 | width: 1.2rem;
206 | height: auto;
207 | -webkit-box-align: center;
208 | -ms-flex-align: center;
209 | align-items: center;
210 | -ms-flex-pack: distribute;
211 | justify-content: space-around;
212 | }
213 |
214 | .bars span {
215 | display: -webkit-box;
216 | display: -ms-flexbox;
217 | display: flex;
218 | width: 100%;
219 | height: 0.16rem;
220 | margin: 0.08rem auto;
221 | -webkit-transition: all 0.3s ease-in-out;
222 | -o-transition: all 0.3s ease-in-out;
223 | transition: all 0.3s ease-in-out;
224 | }
225 |
226 | .bars span:nth-child(1) {
227 | background-color: var(--color_A);
228 | }
229 |
230 | .bars span:nth-child(2) {
231 | background-color: var(--color_B);
232 | }
233 |
234 | .bars span:nth-child(3) {
235 | background-color: var(--color_C);
236 | }
237 |
238 | main {
239 | display: -webkit-box;
240 | display: -ms-flexbox;
241 | display: flex;
242 | -webkit-box-orient: vertical;
243 | -webkit-box-direction: normal;
244 | -ms-flex-direction: column;
245 | flex-direction: column;
246 | margin-top: 5rem;
247 | -webkit-box-align: center;
248 | -ms-flex-align: center;
249 | align-items: center;
250 | }
251 |
252 | .title,
253 | .description {
254 | display: -webkit-box;
255 | display: -ms-flexbox;
256 | display: flex;
257 | padding: 0;
258 | height: 10%;
259 | -webkit-box-pack: center;
260 | -ms-flex-pack: center;
261 | justify-content: center;
262 | -webkit-box-align: center;
263 | -ms-flex-align: center;
264 | align-items: center;
265 | text-align: center;
266 | font-variant-caps: petite-caps;
267 | }
268 |
269 | .description {
270 | font-size: 1.4rem;
271 | font-weight: 600;
272 | }
273 |
274 | main .poster,
275 | main .guide,
276 | main .gallery,
277 | main .privacy {
278 | display: -webkit-box;
279 | display: -ms-flexbox;
280 | display: flex;
281 | -webkit-box-orient: horizontal;
282 | -webkit-box-direction: normal;
283 | -ms-flex-flow: row;
284 | flex-flow: row;
285 | width: calc(100% - 1rem);
286 | height: calc(100vh - 6.5rem);
287 | padding: 0.5rem;
288 | -webkit-box-pack: space-evenly;
289 | -ms-flex-pack: space-evenly;
290 | justify-content: space-evenly;
291 | }
292 |
293 | main .guide,
294 | main .gallery,
295 | main .privacy {
296 | -webkit-box-orient: vertical;
297 | -webkit-box-direction: normal;
298 | -ms-flex-flow: column;
299 | flex-flow: column;
300 | }
301 |
302 | .poster .section-left,
303 | .poster .section-right {
304 | display: -webkit-box;
305 | display: -ms-flexbox;
306 | display: flex;
307 | -webkit-box-orient: vertical;
308 | -webkit-box-direction: normal;
309 | -ms-flex-flow: column;
310 | flex-flow: column;
311 | width: calc(50% - 0.5rem);
312 | -webkit-box-pack: justify;
313 | -ms-flex-pack: justify;
314 | justify-content: space-between;
315 | -webkit-box-align: center;
316 | -ms-flex-align: center;
317 | align-items: center;
318 | }
319 |
320 | .section-left .brand,
321 | .section-right .feature {
322 | display: -webkit-box;
323 | display: -ms-flexbox;
324 | display: flex;
325 | -webkit-box-orient: vertical;
326 | -webkit-box-direction: normal;
327 | -ms-flex-flow: column;
328 | flex-flow: column;
329 | width: 90%;
330 | height: calc(55% - 2rem);
331 | padding: 1rem 0;
332 | -webkit-box-pack: space-evenly;
333 | -ms-flex-pack: space-evenly;
334 | justify-content: space-evenly;
335 | -webkit-box-align: center;
336 | -ms-flex-align: center;
337 | align-items: center;
338 | background-color: var(--color_0);
339 | }
340 |
341 | .brand .logo {
342 | display: -webkit-box;
343 | display: -ms-flexbox;
344 | display: flex;
345 | height: calc(50% - 0.5rem);
346 | padding: 0.25rem;
347 | -webkit-box-pack: center;
348 | -ms-flex-pack: center;
349 | justify-content: center;
350 | -webkit-box-align: center;
351 | -ms-flex-align: center;
352 | align-items: center;
353 | }
354 |
355 | .logo img {
356 | width: 10rem;
357 | height: 10rem;
358 | content: url("extension/icons/Save_Tabs_128.png");
359 | }
360 |
361 | .brand .title {
362 | height: 20%;
363 | }
364 |
365 | .brand .description {
366 | font-size: 1.8rem;
367 | height: 20%;
368 | }
369 |
370 | .section-left .brand-download,
371 | .section-right .source-code {
372 | display: -webkit-box;
373 | display: -ms-flexbox;
374 | display: flex;
375 | -webkit-box-orient: vertical;
376 | -webkit-box-direction: normal;
377 | -ms-flex-flow: column;
378 | flex-flow: column;
379 | height: calc(40% - 2rem);
380 | width: 90%;
381 | padding: 1rem 0;
382 | -webkit-box-pack: space-evenly;
383 | -ms-flex-pack: space-evenly;
384 | justify-content: space-evenly;
385 | -webkit-box-align: center;
386 | -ms-flex-align: center;
387 | align-items: center;
388 | background-color: var(--color_0);
389 | }
390 |
391 | .brand-download .title,
392 | .source-code .title {
393 | height: 20%;
394 | }
395 |
396 | .brand-download .links,
397 | .source-code .links {
398 | display: -webkit-box;
399 | display: -ms-flexbox;
400 | display: flex;
401 | -webkit-box-orient: horizontal;
402 | -webkit-box-direction: normal;
403 | -ms-flex-flow: row wrap;
404 | flex-flow: row wrap;
405 | width: 100%;
406 | height: 80%;
407 | -webkit-box-pack: space-evenly;
408 | -ms-flex-pack: space-evenly;
409 | justify-content: space-evenly;
410 | -webkit-box-align: center;
411 | -ms-flex-align: center;
412 | align-items: center;
413 | }
414 |
415 | .links a {
416 | display: -webkit-box;
417 | display: -ms-flexbox;
418 | display: flex;
419 | padding: 0.25rem;
420 | width: auto;
421 | height: 4rem;
422 | cursor: pointer;
423 | text-decoration: none;
424 | }
425 |
426 | .links a:hover {
427 | -webkit-box-shadow: 0 0 0.4rem var(--color_shadow);
428 | box-shadow: 0 0 0.4rem var(--color_shadow);
429 | }
430 |
431 | .links img,
432 | .links .github {
433 | -o-object-fit: cover;
434 | object-fit: cover;
435 | width: 14rem;
436 | height: 4rem;
437 | }
438 |
439 | .links .github {
440 | display: -webkit-box;
441 | display: -ms-flexbox;
442 | display: flex;
443 | -webkit-box-align: center;
444 | -ms-flex-align: center;
445 | align-items: center;
446 | -webkit-box-pack: space-evenly;
447 | -ms-flex-pack: space-evenly;
448 | justify-content: space-evenly;
449 | }
450 |
451 | .links .github svg {
452 | -o-object-fit: cover;
453 | object-fit: cover;
454 | width: 3rem;
455 | height: 3rem;
456 | }
457 |
458 | .links .button-text {
459 | font-size: 1.5rem;
460 | }
461 |
462 | .poster .vertical-line {
463 | display: -webkit-box;
464 | display: -ms-flexbox;
465 | display: flex;
466 | -ms-flex-item-align: center;
467 | align-self: center;
468 | width: 0.3rem;
469 | height: 70%;
470 | -webkit-box-shadow: inset 0 0 0.3rem var(--color_shadow);
471 | box-shadow: inset 0 0 0.3rem var(--color_shadow);
472 | border-radius: 2rem;
473 | }
474 |
475 | .feature .title {
476 | height: 15%;
477 | width: 40%;
478 | border-bottom: 0.1rem solid var(--color_1);
479 | }
480 |
481 | .feature ul {
482 | display: -webkit-box;
483 | display: -ms-flexbox;
484 | display: flex;
485 | -webkit-box-orient: vertical;
486 | -webkit-box-direction: normal;
487 | -ms-flex-flow: column;
488 | flex-flow: column;
489 | height: 85%;
490 | -webkit-box-pack: space-evenly;
491 | -ms-flex-pack: space-evenly;
492 | justify-content: space-evenly;
493 | }
494 |
495 | .feature ul li {
496 | font-size: 1.4rem;
497 | }
498 |
499 | .guide .content,
500 | .gallery .content,
501 | .privacy .content {
502 | display: -webkit-box;
503 | display: -ms-flexbox;
504 | display: flex;
505 | -webkit-box-orient: horizontal;
506 | -webkit-box-direction: normal;
507 | -ms-flex-flow: row;
508 | flex-flow: row;
509 | -webkit-box-pack: space-evenly;
510 | -ms-flex-pack: space-evenly;
511 | justify-content: space-evenly;
512 | height: 90%;
513 | }
514 |
515 | .content .export,
516 | .content .import,
517 | .content .logs {
518 | display: -webkit-box;
519 | display: -ms-flexbox;
520 | display: flex;
521 | -webkit-box-orient: vertical;
522 | -webkit-box-direction: normal;
523 | -ms-flex-flow: column;
524 | flex-flow: column;
525 | width: calc(32% - 1rem);
526 | padding: 1rem 0.5rem;
527 | -webkit-box-align: center;
528 | -ms-flex-align: center;
529 | align-items: center;
530 | background-color: var(--color_0);
531 | }
532 |
533 | .content .title {
534 | padding: 0.25rem 0;
535 | height: calc(5% - 0.6rem);
536 | border-bottom: 0.1rem solid;
537 | }
538 |
539 | .content .preview {
540 | display: -webkit-box;
541 | display: -ms-flexbox;
542 | display: flex;
543 | margin-top: 0.5rem;
544 | padding: 0.5rem;
545 | width: calc(100% - 1rem);
546 | height: calc(40% - 1.5rem);
547 | overflow: hidden;
548 | -webkit-box-pack: center;
549 | -ms-flex-pack: center;
550 | justify-content: center;
551 | }
552 |
553 | .export .preview {
554 | -webkit-box-shadow: inset 0 0 0.15rem var(--color_A_light_2);
555 | box-shadow: inset 0 0 0.15rem var(--color_A_light_2);
556 | background-color: var(--color_A_light_1);
557 | }
558 |
559 | .import .preview {
560 | -webkit-box-shadow: inset 0 0 0.15rem var(--color_B_light_2);
561 | box-shadow: inset 0 0 0.15rem var(--color_B_light_2);
562 | background-color: var(--color_B_light_1);
563 | }
564 |
565 | .logs .preview {
566 | -webkit-box-shadow: inset 0 0 0.15rem var(--color_C_light_2);
567 | box-shadow: inset 0 0 0.15rem var(--color_C_light_2);
568 | background-color: var(--color_C_light_1);
569 | }
570 |
571 | .preview img {
572 | display: -webkit-box;
573 | display: -ms-flexbox;
574 | display: flex;
575 | -o-object-fit: scale-down;
576 | object-fit: scale-down;
577 | width: 100%;
578 | height: auto;
579 | }
580 |
581 | .content .points {
582 | display: -webkit-box;
583 | display: -ms-flexbox;
584 | display: flex;
585 | -webkit-box-orient: vertical;
586 | -webkit-box-direction: normal;
587 | -ms-flex-flow: column;
588 | flex-flow: column;
589 | overflow-y: auto;
590 | margin-top: 1rem;
591 | width: 100%;
592 | height: calc(55% - 1rem);
593 | }
594 |
595 | .points .title {
596 | margin-top: 1rem;
597 | height: 5%;
598 | font-size: 1.2rem;
599 | -webkit-box-pack: left;
600 | -ms-flex-pack: left;
601 | justify-content: left;
602 | }
603 |
604 | .points ol {
605 | display: -webkit-box;
606 | display: -ms-flexbox;
607 | display: flex;
608 | -webkit-box-orient: vertical;
609 | -webkit-box-direction: normal;
610 | -ms-flex-flow: column;
611 | flex-flow: column;
612 | }
613 |
614 | /* Screenshots */
615 |
616 | .gallery .content {
617 | padding: 0 0.5rem;
618 | width: calc(100% - 1rem);
619 | height: 90%;
620 | -webkit-box-align: center;
621 | -ms-flex-align: center;
622 | align-items: center;
623 | -webkit-box-pack: center;
624 | -ms-flex-pack: center;
625 | justify-content: center;
626 | }
627 |
628 | .gallery .screenshots {
629 | display: -webkit-box;
630 | display: -ms-flexbox;
631 | display: flex;
632 | -webkit-box-orient: horizontal;
633 | -webkit-box-direction: normal;
634 | -ms-flex-flow: row;
635 | flex-flow: row;
636 | padding: 0 0.5rem;
637 | width: calc(100% - 1rem);
638 | height: 100%;
639 | -webkit-box-align: center;
640 | -ms-flex-align: center;
641 | align-items: center;
642 | overflow-x: auto;
643 | }
644 |
645 | .screenshots .screenshot-content {
646 | display: -webkit-box;
647 | display: -ms-flexbox;
648 | display: flex;
649 | -webkit-box-flex: 0;
650 | -ms-flex: 0 0 auto;
651 | flex: 0 0 auto;
652 | -webkit-box-orient: vertical;
653 | -webkit-box-direction: normal;
654 | -ms-flex-flow: column;
655 | flex-flow: column;
656 | padding: 1rem;
657 | width: calc(33.3% - 2rem);
658 | height: calc(100% - 2rem);
659 | -webkit-box-pack: center;
660 | -ms-flex-pack: center;
661 | justify-content: center;
662 | }
663 |
664 | .screenshot-content .body {
665 | display: -webkit-box;
666 | display: -ms-flexbox;
667 | display: flex;
668 | -webkit-box-orient: vertical;
669 | -webkit-box-direction: normal;
670 | -ms-flex-flow: column;
671 | flex-flow: column;
672 | padding: 0.5rem;
673 | width: auto;
674 | height: calc(100% - 1rem);
675 | background-color: var(--color_0);
676 | }
677 |
678 | .body .image {
679 | display: -webkit-box;
680 | display: -ms-flexbox;
681 | display: flex;
682 | height: calc(95% - 0.5rem);
683 | padding: 0.25rem;
684 | }
685 |
686 | .image img {
687 | -o-object-fit: scale-down;
688 | object-fit: scale-down;
689 | width: 100%;
690 | }
691 |
692 | .body .description {
693 | font-size: 1rem;
694 | font-variant-caps: normal;
695 | -webkit-box-shadow: inset 0 0 0.1rem var(--color_shadow);
696 | box-shadow: inset 0 0 0.1rem var(--color_shadow);
697 | }
698 |
699 | /* Privacy */
700 |
701 | .privacy .content {
702 | font-size: 3rem;
703 | -webkit-box-align: center;
704 | -ms-flex-align: center;
705 | align-items: center;
706 | -webkit-box-pack: center;
707 | -ms-flex-pack: center;
708 | justify-content: center;
709 | background-color: var(--color_0);
710 | }
711 |
712 | .content .privacy-content {
713 | text-align: center;
714 | }
715 |
716 | /* Footer */
717 |
718 | footer {
719 | margin-top: 0.5rem;
720 | margin-bottom: 0.5rem;
721 | height: 2rem;
722 | -webkit-box-pack: center;
723 | -ms-flex-pack: center;
724 | justify-content: center;
725 | font-variant-caps: petite-caps;
726 | font-weight: 500;
727 | }
728 |
729 | footer .developer-name {
730 | padding: 0 0.25rem;
731 | font-weight: bold;
732 | }
733 |
734 | /* Extras */
735 | .border-radius-type-A {
736 | border-radius: 0.5rem;
737 | }
738 |
739 | .border-shadow {
740 | -webkit-box-shadow: 0 0 0.2rem var(--color_shadow);
741 | box-shadow: 0 0 0.2rem var(--color_shadow);
742 | }
743 |
744 | ::-webkit-scrollbar {
745 | width: 0.5rem;
746 | height: 0.5rem;
747 | }
748 |
749 | ::-webkit-scrollbar-thumb {
750 | border-radius: 0.5rem;
751 | -webkit-box-shadow: inset 0 0 0.5rem var(--color_shadow);
752 | box-shadow: inset 0 0 0.5rem var(--color_shadow);
753 | }
754 |
755 | @media only screen and (max-width: 768px) {
756 | html {
757 | scroll-padding-top: 4rem;
758 | }
759 |
760 | body {
761 | padding: 0 1rem;
762 | }
763 |
764 | h2 {
765 | font-size: 3rem;
766 | }
767 |
768 | h3 {
769 | font-size: 2rem;
770 | }
771 |
772 | header {
773 | left: 1rem;
774 | width: calc(100% - 2rem);
775 | height: 3rem;
776 | padding: 0.5rem 0;
777 | }
778 |
779 | header.active {
780 | height: calc(100% - 1rem);
781 | }
782 |
783 | header.border-shadow {
784 | -webkit-box-shadow: none;
785 | box-shadow: none;
786 | }
787 |
788 | nav {
789 | -webkit-box-orient: vertical;
790 | -webkit-box-direction: normal;
791 | -ms-flex-direction: column;
792 | flex-direction: column;
793 | -webkit-box-pack: start;
794 | -ms-flex-pack: start;
795 | justify-content: flex-start;
796 | padding: 0;
797 | border: none;
798 | }
799 |
800 | header.active nav {
801 | -webkit-box-shadow: none;
802 | box-shadow: none;
803 | }
804 |
805 | nav .nav-left {
806 | width: calc(100% - 1rem);
807 | height: 2.9rem;
808 | padding: 0 0.5rem;
809 | -webkit-box-align: center;
810 | -ms-flex-align: center;
811 | align-items: center;
812 | border: 0.05rem solid;
813 | border-color: var(--color_A) var(--color_B) var(--color_C);
814 | -webkit-box-shadow: 0 0 0.2rem var(--color_shadow);
815 | box-shadow: 0 0 0.2rem var(--color_shadow);
816 | }
817 |
818 | .nav-logo img {
819 | content: url("extension/icons/Save_Tabs_32.png");
820 | }
821 |
822 | .nav-logo span {
823 | padding: 0 0.25rem;
824 | font-size: 1.5rem;
825 | font-weight: 550;
826 | }
827 |
828 | nav ul {
829 | display: none;
830 | -webkit-box-orient: vertical;
831 | -webkit-box-direction: normal;
832 | -ms-flex-direction: column;
833 | flex-direction: column;
834 | width: calc(100% - 0.2rem);
835 | height: 0;
836 | text-align: center;
837 | border: 0.1rem solid;
838 | border-color: var(--color_A) var(--color_B) var(--color_C);
839 | }
840 |
841 | nav ul.active {
842 | display: -webkit-box;
843 | display: -ms-flexbox;
844 | display: flex;
845 | height: auto;
846 | -webkit-box-pack: start;
847 | -ms-flex-pack: start;
848 | justify-content: flex-start;
849 | -webkit-box-shadow: 0 0 0.2rem var(--color_shadow);
850 | box-shadow: 0 0 0.2rem var(--color_shadow);
851 | }
852 |
853 | nav ul li {
854 | width: 60%;
855 | padding: 0.5rem 0;
856 | }
857 |
858 | nav ul li .nav-link {
859 | padding: 0.25rem;
860 | font-size: 1.25rem;
861 | font-weight: 400;
862 | border-radius: 0.25rem;
863 | }
864 |
865 | .nav-left .hamburger {
866 | display: -webkit-box;
867 | display: -ms-flexbox;
868 | display: flex;
869 | -webkit-box-pack: center;
870 | -ms-flex-pack: center;
871 | justify-content: center;
872 | -webkit-box-align: center;
873 | -ms-flex-align: center;
874 | align-items: center;
875 | width: 1.7rem;
876 | height: 1.7rem;
877 | border-radius: 50%;
878 | cursor: pointer;
879 | -webkit-box-shadow: 0 0 0.2rem var(--color_shadow);
880 | box-shadow: 0 0 0.2rem var(--color_shadow);
881 | }
882 |
883 | .hamburger.active span:nth-child(2) {
884 | opacity: 0;
885 | }
886 |
887 | .hamburger.active span:nth-child(1) {
888 | -webkit-transform: translateY(0.32rem) rotate(45deg);
889 | -ms-transform: translateY(0.32rem) rotate(45deg);
890 | transform: translateY(0.32rem) rotate(45deg);
891 | }
892 |
893 | .hamburger.active span:nth-child(3) {
894 | -webkit-transform: translateY(-0.32rem) rotate(-45deg);
895 | -ms-transform: translateY(-0.32rem) rotate(-45deg);
896 | transform: translateY(-0.32rem) rotate(-45deg);
897 | }
898 |
899 | .scroll-lock {
900 | overflow: hidden;
901 | }
902 |
903 | main {
904 | margin-top: 4rem;
905 | }
906 |
907 | main .poster,
908 | main .guide {
909 | -webkit-box-orient: horizontal;
910 | -webkit-box-direction: normal;
911 | -ms-flex-flow: row wrap;
912 | flex-flow: row wrap;
913 | height: auto;
914 | padding: 0 0.25rem;
915 | -webkit-box-pack: justify;
916 | -ms-flex-pack: justify;
917 | justify-content: space-between;
918 | }
919 |
920 | main .gallery,
921 | main .privacy {
922 | height: calc(100vh - 5.5rem);
923 | }
924 |
925 | .poster .section-left,
926 | .poster .section-right {
927 | width: 100%;
928 | height: calc(100vh - 4rem);
929 | -webkit-box-pack: space-evenly;
930 | -ms-flex-pack: space-evenly;
931 | justify-content: space-evenly;
932 | }
933 |
934 | .section-left .brand,
935 | .section-right .feature {
936 | width: 100%;
937 | height: calc(55% - 1rem);
938 | padding: 0.5rem 0;
939 | -webkit-box-shadow: 0 0 0.1rem var(--color_shadow);
940 | box-shadow: 0 0 0.1rem var(--color_shadow);
941 | }
942 |
943 | .section-right .feature {
944 | height: calc(60% - 1rem);
945 | }
946 |
947 | .brand .logo {
948 | height: calc(50% - 0.25rem);
949 | padding: 0.125rem;
950 | }
951 |
952 | .logo img {
953 | width: 6rem;
954 | height: 6rem;
955 | }
956 |
957 | .brand .description {
958 | font-size: 1.2rem;
959 | height: 20%;
960 | }
961 |
962 | .section-left .brand-download,
963 | .section-right .source-code {
964 | -webkit-box-orient: vertical;
965 | -webkit-box-direction: normal;
966 | -ms-flex-flow: column;
967 | flex-flow: column;
968 | height: calc(40% - 1rem);
969 | width: 100%;
970 | padding: 0.5rem 0;
971 | }
972 |
973 | .section-right .source-code {
974 | height: calc(35% - 1rem);
975 | }
976 |
977 | .brand-download .links,
978 | .source-code .links {
979 | -webkit-box-orient: vertical;
980 | -webkit-box-direction: normal;
981 | -ms-flex-flow: column;
982 | flex-flow: column;
983 | }
984 |
985 | .links a {
986 | padding: 0.125rem;
987 | height: 3rem;
988 | }
989 |
990 | .links img,
991 | .links .github {
992 | width: 10.5rem;
993 | height: 3rem;
994 | }
995 |
996 | .links .github svg {
997 | width: 2rem;
998 | height: 2rem;
999 | }
1000 |
1001 | .links .button-text {
1002 | font-size: 1.2rem;
1003 | }
1004 |
1005 | .poster .vertical-line {
1006 | margin-left: 15%;
1007 | width: 70%;
1008 | height: 0.3rem;
1009 | }
1010 |
1011 | .feature ul li {
1012 | font-size: 1rem;
1013 | }
1014 |
1015 | main .guide,
1016 | main .gallery,
1017 | main .privacy {
1018 | -webkit-box-pack: center;
1019 | -ms-flex-pack: center;
1020 | justify-content: center;
1021 | }
1022 |
1023 | .guide .content,
1024 | .gallery .content,
1025 | .privacy .content {
1026 | -webkit-box-orient: vertical;
1027 | -webkit-box-direction: normal;
1028 | -ms-flex-flow: column;
1029 | flex-flow: column;
1030 | -webkit-box-pack: space-evenly;
1031 | -ms-flex-pack: space-evenly;
1032 | justify-content: space-evenly;
1033 | }
1034 |
1035 | .content .export,
1036 | .content .import,
1037 | .content .logs {
1038 | margin: 0.5rem 0;
1039 | width: calc(100% - 0.5rem);
1040 | height: auto;
1041 | padding: 0.5rem 0.25rem;
1042 | }
1043 |
1044 | .content .preview {
1045 | padding: 0.25rem;
1046 | width: calc(100% - 0.5rem);
1047 | height: calc(40% - 1rem);
1048 | }
1049 |
1050 | .content .points {
1051 | margin-top: 0.5rem;
1052 | height: calc(55% - 0.5rem);
1053 | }
1054 |
1055 | .points .title {
1056 | margin-top: 0.5rem;
1057 | font-size: 1.2rem;
1058 | }
1059 |
1060 | /* Screenshots */
1061 |
1062 | .gallery .content {
1063 | padding: 0 0.25rem;
1064 | width: calc(100% - 0.5rem);
1065 | /* background-color: red; */
1066 | }
1067 |
1068 | .gallery .screenshots {
1069 | padding: 0 0.25rem;
1070 | width: calc(100% - 0.5rem);
1071 | }
1072 |
1073 | .screenshots .screenshot-content {
1074 | padding: 0.5rem;
1075 | width: calc(100% - 1rem);
1076 | height: calc(100% - 1rem);
1077 | }
1078 |
1079 | .screenshot-content .body {
1080 | padding: 0.25rem;
1081 | height: calc(100% - 0.5rem);
1082 | }
1083 |
1084 | .body .image {
1085 | height: calc(90% - 0.5rem);
1086 | }
1087 |
1088 | .body .description {
1089 | font-size: 0.8rem;
1090 | }
1091 |
1092 | /* Footer */
1093 |
1094 | footer {
1095 | display: -webkit-box;
1096 | display: -ms-flexbox;
1097 | display: flex;
1098 | -webkit-box-orient: vertical;
1099 | -webkit-box-direction: normal;
1100 | -ms-flex-direction: column;
1101 | flex-direction: column;
1102 | padding: 0;
1103 | height: 2rem;
1104 | -webkit-box-pack: center;
1105 | -ms-flex-pack: center;
1106 | justify-content: center;
1107 | -webkit-box-align: center;
1108 | -ms-flex-align: center;
1109 | align-items: center;
1110 | height: 2rem;
1111 | font-size: 0.9rem;
1112 | }
1113 |
1114 | footer .developer-name {
1115 | padding: 0 0.125rem;
1116 | font-weight: bold;
1117 | }
1118 |
1119 | .border-radius-type-A {
1120 | border-radius: 0.25rem;
1121 | }
1122 |
1123 | .border-shadow {
1124 | -webkit-box-shadow: 0 0 0.1rem var(--color_shadow);
1125 | box-shadow: 0 0 0.1rem var(--color_shadow);
1126 | }
1127 | }
1128 |
--------------------------------------------------------------------------------