├── .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 |
2 | 3 | Save Tabs 4 | 5 |

Save Tabs

6 |
7 | 8 | [![Chrome Web Store](https://img.shields.io/chrome-web-store/v/ljokfgphjbhjheflldgfmjligcmcmhmn.svg?style=plastic)](https://chrome.google.com/webstore/detail/detail/save-tabs/ljokfgphjbhjheflldgfmjligcmcmhmn) [![Firefox Add-ons](https://img.shields.io/amo/v/save-tabs.svg?style=plastic)](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 | Chrome Firefox 35 | 36 | 37 | ## ⚙️ Install 38 | 39 | ### From Web Store 40 | Get it on Chrome Webstore 41 | 42 | Get it on Chrome Webstore 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 | 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 |
26 | 58 |
59 | 60 |
61 |
62 |
63 |
64 | 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 | Export Guide 182 |
183 |
184 | 185 | * Assuming multiple tabs are open in 187 | browser. 189 | 190 |
191 | # Default Export 192 |
193 |
    194 |
  1. Open Save Tabs Extension.
  2. 195 |
  3. Click on Export button.
  4. 196 |
  5. Native File Explorer open to save file.
  6. 197 |
  7. Click on Save.
  8. 198 |
199 |
200 | # Custom Name Export 201 |
202 |
    203 |
  1. Open Save Tabs Extension.
  2. 204 |
  3. 205 | Enter custom name in 206 | "Enter file name". (Ex. 207 | Research-Project-X) 208 |
  4. 209 |
  5. Click on Export button.
  6. 210 |
  7. Native File Explorer open to save file.
  8. 211 |
  9. Click on Save.
  10. 212 |
213 | Tabs will be exported successfully in file. 214 |
215 |
216 |
217 |
218 |

Import

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

Logs

251 |
252 |
253 | Logging Guide 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 |
  1. Open Save Tabs Extension.
  2. 268 |
  3. 269 | Click on toggle button next to 270 | Logs title. 271 |
  4. 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 | 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 | --------------------------------------------------------------------------------