├── .circleci └── config.yml ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintrc ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── addon ├── background.js ├── buildSettings.js.tmpl ├── images │ ├── close-16-light.svg │ ├── close-16.svg │ ├── globe.svg │ ├── in-content-clouds-dark.png │ ├── in-content-clouds.png │ ├── in-content-icon.png │ ├── side-view-tour-1.jpg │ ├── side-view-tour-2.jpg │ ├── side-view-tour-3.jpg │ └── side-view-tour-4.jpg ├── intro.css ├── intro.html ├── intro.js ├── manifest.json.tmpl ├── popup.css ├── popup.html ├── popup.js ├── side-view-badged.svg ├── side-view-onboarding.svg ├── side-view.png ├── side-view.svg ├── sidebar.css ├── sidebar.html └── sidebar.js ├── docs └── release.md ├── package-lock.json └── package.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.0 2 | jobs: 3 | package: 4 | docker: 5 | - image: circleci/node:8.9.4 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - node-modules- 11 | - run: 12 | name: install 13 | command: npm install 14 | - run: 15 | name: package 16 | command: npm run package 17 | - run: 18 | name: test 19 | command: npm test 20 | - save_cache: 21 | key: node-modules- 22 | paths: 23 | - node_modules 24 | - store_artifacts: 25 | path: addon.xpi 26 | - persist_to_workspace: 27 | root: . 28 | paths: 29 | - ./* 30 | workflows: 31 | version: 2 32 | package: 33 | jobs: 34 | - package 35 | 36 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | addon/build 2 | Profile 3 | web-ext-artifacts 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "webextensions": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:mozilla/recommended" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": 8, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "mozilla" 16 | ], 17 | "root": true, 18 | "rules": { 19 | "eqeqeq": "error", 20 | "no-console": "warn", 21 | "space-before-function-paren": "off", 22 | "no-console": ["error", {"allow": ["error", "info", "trace", "warn"]}] 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | web-ext-artifacts 3 | addon*.xpi 4 | addon/build 5 | addon/manifest.json 6 | /addon/experiment/study 7 | /null-addon/addon/experiment 8 | .DS_Store 9 | /Profile 10 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "rules": { 4 | "font-family-no-missing-generic-family-keyword": null 5 | } 6 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Community Participation Guidelines 2 | 3 | This repository is governed by Mozilla's code of conduct and etiquette guidelines. 4 | For more details, please read the 5 | [Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). 6 | 7 | ## How to Report 8 | For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. 9 | 10 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Side View 2 | 3 | An experiment with opening mobile views of pages in the sidebar. 4 | 5 | [**Install from addons.mozilla.org**](https://addons.mozilla.org/en-US/firefox/addon/side-view/) 6 | 7 | ## Installing 8 | 9 | Use `npm install`, then `npm start`. 10 | 11 | ## Installing manually 12 | 13 | Check out the repository. Go to `about:debugging` in Firefox, and select **Load Temporary Add-on**. Select a file in the `addon/` directory. 14 | 15 | Or: install [`web-ext`](https://github.com/mozilla/web-ext) (like `npm i -g web-ext`) and run `web-ext run -s addon/ --browser-console -f nightly` 16 | 17 | ## Using 18 | 19 | This adds a context menu item: **Open in sidebar** or **Open link in sidebar**. Select that, and the sidebar will be opened with a mobile view of the page. 20 | 21 | ## Credits 22 | 23 | [Anthony_f](https://addons.mozilla.org/en-US/firefox/user/Anthony_f/)'s [Sidebar for Google Search](https://addons.mozilla.org/en-US/firefox/addon/sidebar-for-google-search/) inspired this add-on's approach. 24 | -------------------------------------------------------------------------------- /addon/background.js: -------------------------------------------------------------------------------- 1 | const FIREFOX_VERSION = /rv:([0-9.]+)/.exec(navigator.userAgent)[1]; 2 | 3 | const USER_AGENT = `Mozilla/5.0 (Android 14; Mobile; rv:${FIREFOX_VERSION}) Gecko/${FIREFOX_VERSION} Firefox/${FIREFOX_VERSION}`; 4 | // iOS: 5 | // Mozilla/5.0 (iPhone; CPU iPhone OS 9_2 like Mac OS X) AppleWebKit/601.1 (KHTML, like Gecko) CriOS/47.0.2526.70 Mobile/13C71 Safari/601.1.46 6 | // Firefox for Android: 7 | // Mozilla/5.0 (Android 4.4; Mobile; rv:41.0) Gecko/41.0 Firefox/41.0 8 | // Chrome for Android: 9 | // Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19 10 | 11 | // If you update DEFAULT_DESKTOP_SITES you should also increment DEFAULT_DESKTOP_VERSION 12 | const DEFAULT_DESKTOP_SITES = [ 13 | "www.youtube.com", 14 | "www.metacafe.com", 15 | "myspace.com", 16 | "imgur.com", 17 | "meet.google.com", 18 | "meet.app.goo.gl", 19 | ]; 20 | 21 | const DEFAULT_DESKTOP_VERSION = 2; 22 | 23 | const MAX_RECENT_TABS = 5; 24 | let sidebarUrl; 25 | let hasSeenPrivateWarning = false; 26 | 27 | browser.contextMenus.create({ 28 | id: "open-in-sidebar", 29 | title: "Open in Side View", 30 | contexts: ["page", "tab", "bookmark"], 31 | documentUrlPatterns: [""], 32 | }); 33 | 34 | browser.contextMenus.create({ 35 | id: "open-link-in-sidebar", 36 | title: "Open link in Side View", 37 | // FIXME: could add "bookmark", but have to fetch by info.bookmarkId 38 | contexts: ["link"], 39 | documentUrlPatterns: [""], 40 | }); 41 | 42 | browser.contextMenus.onClicked.addListener(async (info, tab) => { 43 | let url; 44 | let favIconUrl; 45 | let title; 46 | let incognito = tab && tab.incognito; 47 | await browser.sidebarAction.open(); 48 | if (info.linkUrl) { 49 | url = info.linkUrl; 50 | } else if (info.bookmarkId) { 51 | let bookmarkInfo = await browser.bookmarks.get(info.bookmarkId); 52 | url = bookmarkInfo[0].url; 53 | } else { 54 | url = tab.url; 55 | title = tab.title; 56 | favIconUrl = tab.favIconUrl; 57 | } 58 | if (title && !incognito) { 59 | // In cases when we can't get a good title and favicon, we just don't bother saving it as a recent tab 60 | addRecentTab({url, favIconUrl, title}); 61 | } 62 | openUrl(url); 63 | }); 64 | 65 | browser.pageAction.onClicked.addListener((async (tab) => { 66 | let url = tab.url; 67 | if (!tab.incognito) { 68 | addRecentTab({url, favIconUrl: tab.favIconUrl, title: tab.title}); 69 | } 70 | await browser.sidebarAction.open(); 71 | openUrl(url); 72 | })); 73 | 74 | async function openUrl(url) { 75 | sidebarUrl = url; 76 | let hostname = (new URL(url)).hostname; 77 | let isDesktop = !!desktopHostnames[hostname]; 78 | browser.runtime.sendMessage({ 79 | type: "isDesktop", 80 | isDesktop, 81 | }).catch((error) => { 82 | // If the popup is not open this gives an error, but we don't care 83 | }); 84 | browser.sidebarAction.setPanel({panel: url}); 85 | } 86 | 87 | /* eslint-disable consistent-return */ 88 | // Because this dispatches to different kinds of functions, its return behavior is inconsistent 89 | browser.runtime.onMessage.addListener(async (message) => { 90 | if (message.type === "toggleDesktop") { 91 | toggleDesktop(); 92 | } else if (message.type === "openUrl") { 93 | openUrl(message.url); 94 | let windowInfo = await browser.windows.getCurrent(); 95 | if (!windowInfo.incognito) { 96 | addRecentTab(message); 97 | } 98 | } else if (message.type === "dismissTab") { 99 | dismissRecentTab(message.index); 100 | } else if (message.type === "getRecentAndDesktop") { 101 | let isDesktop = false; 102 | if (sidebarUrl) { 103 | let hostname = (new URL(sidebarUrl)).hostname; 104 | isDesktop = !!desktopHostnames[hostname]; 105 | } 106 | let currentWindow = await browser.windows.getCurrent(); 107 | return Promise.resolve({ 108 | recentTabs, 109 | isDesktop, 110 | hasSeenPrivateWarning, 111 | incognito: currentWindow.incognito, 112 | }); 113 | } else if (message.type === "turnOffPrivateWarning") { 114 | turnOffPrivateWarning(); 115 | } else { 116 | console.error("Unexpected message to background:", message); 117 | } 118 | }); 119 | /* eslint-enable consistent-return */ 120 | 121 | // This is a RequestFilter: https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/webRequest/RequestFilter 122 | // It matches tabs that aren't attached to a normal location (like a sidebar) 123 | // It only matches embedded iframes 124 | let requestFilter = { 125 | tabId: -1, 126 | types: ["main_frame"], 127 | urls: ["http://*/*", "https://*/*"], 128 | }; 129 | 130 | let desktopHostnames = {}; 131 | 132 | async function toggleDesktop() { 133 | if (!sidebarUrl) { 134 | console.warn("Got toggle desktop with no known sidebar URL"); 135 | return; 136 | } 137 | let hostname = (new URL(sidebarUrl)).hostname; 138 | let isDesktop = !desktopHostnames[hostname]; 139 | if (isDesktop) { 140 | desktopHostnames[hostname] = true; 141 | } else { 142 | delete desktopHostnames[hostname]; 143 | } 144 | // We can't trigger a real reload without changing the URL, so we change it to blank and then 145 | // back to the previous URL: 146 | browser.sidebarAction.setPanel({panel: "about:blank"}); 147 | openUrl(sidebarUrl); 148 | await browser.storage.local.set({desktopHostnames, defaultDesktopVersion: DEFAULT_DESKTOP_VERSION}); 149 | } 150 | 151 | let recentTabs = []; 152 | 153 | async function addRecentTab(tabInfo) { 154 | recentTabs = recentTabs.filter((item) => item.url !== tabInfo.url); 155 | recentTabs.unshift(tabInfo); 156 | recentTabs.splice(MAX_RECENT_TABS); 157 | try { 158 | await browser.runtime.sendMessage({ 159 | type: "updateRecentTabs", 160 | recentTabs, 161 | }); 162 | } catch (error) { 163 | if (String(error).includes("Could not establish connection")) { 164 | // We're just speculatively sending messages to the popup, it might not be open, 165 | // and that is fine 166 | } else { 167 | console.error("Got updating recent tabs:", String(error), error); 168 | } 169 | } 170 | await browser.storage.local.set({recentTabs}); 171 | } 172 | 173 | async function dismissRecentTab(tab_index) { 174 | recentTabs.splice(tab_index, 1); 175 | try { 176 | await browser.runtime.sendMessage({ 177 | type: "updateRecentTabs", 178 | recentTabs, 179 | }); 180 | 181 | } catch (error) { 182 | if (String(error).includes("Could not establish connection")) { 183 | // popup speculation, as in addRecentTab() 184 | } else { 185 | console.error("Got updating recent tabs:", String(error), error); 186 | } 187 | } 188 | await browser.storage.local.set({recentTabs}); 189 | } 190 | 191 | // Add a mobile header to outgoing requests 192 | browser.webRequest.onBeforeSendHeaders.addListener(function (info) { 193 | let hostname = (new URL(info.url)).hostname; 194 | if (desktopHostnames[hostname]) { 195 | return {}; 196 | } 197 | let headers = info.requestHeaders; 198 | for (let i = 0; i < headers.length; i++) { 199 | let name = headers[i].name.toLowerCase(); 200 | if (name === "user-agent") { 201 | headers[i].value = USER_AGENT; 202 | return {"requestHeaders": headers}; 203 | } 204 | } 205 | return {}; 206 | }, requestFilter, ["blocking", "requestHeaders"]); 207 | 208 | function privateWarningOnUpdated(tabId, changeInfo, tab) { 209 | if (tab.incognito) { 210 | browser.browserAction.setBadgeText({text: "!", tabId: tab.id}); 211 | } 212 | } 213 | 214 | async function turnOffPrivateWarning() { 215 | hasSeenPrivateWarning = true; 216 | browser.tabs.onUpdated.removeListener(privateWarningOnUpdated); 217 | let win = await browser.windows.getCurrent({populate: true}); 218 | for (let tab of win.tabs) { 219 | browser.browserAction.setBadgeText({text: null, tabId: tab.id}); 220 | } 221 | await browser.storage.local.set({hasSeenPrivateWarning}); 222 | } 223 | 224 | function showOnboardingBadge() { 225 | browser.browserAction.setIcon({path: "side-view-onboarding.svg"}); 226 | function onBrowserActionClick() { 227 | browser.browserAction.setPopup({popup: "intro.html"}); 228 | browser.browserAction.openPopup(); 229 | browser.browserAction.onClicked.removeListener(onBrowserActionClick); 230 | browser.browserAction.setIcon({path: "side-view.svg"}); 231 | browser.storage.local.set({hasBeenOnboarded: true}); 232 | browser.browserAction.setPopup({popup: "popup.html"}); 233 | } 234 | // This disables the default popup action and lets us intercept the clicks: 235 | browser.browserAction.setPopup({popup: ""}); 236 | browser.browserAction.onClicked.addListener(onBrowserActionClick); 237 | } 238 | 239 | async function init() { 240 | const result = await browser.storage.local.get(["desktopHostnames", "defaultDesktopVersion", "recentTabs", "hasSeenPrivateWarning", "hasBeenOnboarded"]); 241 | if (!result.desktopHostnames) { 242 | desktopHostnames = {}; 243 | } else { 244 | desktopHostnames = result.desktopHostnames; 245 | } 246 | if (!result.defaultDesktopVersion || result.defaultDesktopVersion < DEFAULT_DESKTOP_VERSION) { 247 | for (let hostname of DEFAULT_DESKTOP_SITES) { 248 | desktopHostnames[hostname] = true; 249 | } 250 | } 251 | recentTabs = result.recentTabs || []; 252 | hasSeenPrivateWarning = result.hasSeenPrivateWarning; 253 | if (!hasSeenPrivateWarning) { 254 | browser.tabs.onUpdated.addListener(privateWarningOnUpdated); 255 | } 256 | if (!result.hasBeenOnboarded) { 257 | showOnboardingBadge(); 258 | } 259 | } 260 | 261 | init(); 262 | -------------------------------------------------------------------------------- /addon/buildSettings.js.tmpl: -------------------------------------------------------------------------------- 1 | var buildSettings = { 2 | NODE_ENV: "{{NODE_ENV}}", 3 | }; 4 | -------------------------------------------------------------------------------- /addon/images/close-16-light.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /addon/images/close-16.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /addon/images/globe.svg: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /addon/images/in-content-clouds-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/in-content-clouds-dark.png -------------------------------------------------------------------------------- /addon/images/in-content-clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/in-content-clouds.png -------------------------------------------------------------------------------- /addon/images/in-content-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/in-content-icon.png -------------------------------------------------------------------------------- /addon/images/side-view-tour-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/side-view-tour-1.jpg -------------------------------------------------------------------------------- /addon/images/side-view-tour-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/side-view-tour-2.jpg -------------------------------------------------------------------------------- /addon/images/side-view-tour-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/side-view-tour-3.jpg -------------------------------------------------------------------------------- /addon/images/side-view-tour-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/images/side-view-tour-4.jpg -------------------------------------------------------------------------------- /addon/intro.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --blue-40: #45a1ff; 3 | --blue-50: #0a84ff; 4 | --blue-50-a30: rgba(10, 132, 255, 0.3); 5 | --blue-60: #0060df; 6 | --blue-70: #003eaa; 7 | --blue-80: #002275; 8 | --blue-90: #000f40; 9 | } 10 | 11 | #container { 12 | display: flex; 13 | flex-direction: column; 14 | width: 100%; 15 | font: message-box; 16 | font-size: 16px; 17 | } 18 | 19 | .slide { 20 | width: 480px; 21 | flex-direction: column; 22 | align-items: center; 23 | } 24 | 25 | .slide-image { 26 | width: 480px; 27 | flex: 0 0 270px; 28 | border-bottom: 1px solid #ddd; 29 | } 30 | 31 | .slide p { 32 | line-height: 1.5em; 33 | margin: 0; 34 | padding: 0 32px; 35 | text-align: center; 36 | height: 84px; 37 | display: flex; 38 | align-items: center; 39 | color: black; 40 | background: white; 41 | } 42 | 43 | .slide-displayed { 44 | display: flex; 45 | } 46 | 47 | .slide-hidden { 48 | display: none; 49 | } 50 | 51 | #slide-controls { 52 | align-items: center; 53 | padding: 0 16px 16px; 54 | display: grid; 55 | grid-template-columns: 120px 1fr 120px; 56 | justify-content: center; 57 | } 58 | 59 | button { 60 | align-items: center; 61 | background: #f2f2f2; 62 | border-radius: 2px; 63 | border: 0; 64 | display: flex; 65 | font-size: 14px; 66 | height: 32px; 67 | justify-content: center; 68 | padding: 5px; 69 | flex: 1; 70 | } 71 | 72 | .button-hidden { 73 | display: none; 74 | } 75 | 76 | .button-invisible { 77 | visibility: hidden; 78 | } 79 | 80 | #next, 81 | #done { 82 | grid-column: 3 / -1; 83 | } 84 | 85 | .button-primary { 86 | background: var(--blue-50); 87 | color: white; 88 | } 89 | 90 | .button-primary:hover, 91 | .button-primary:focus { 92 | background: var(--blue-60); 93 | } 94 | 95 | .button-primary:active { 96 | background: var(--blue-70); 97 | } 98 | -------------------------------------------------------------------------------- /addon/intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | 12 |
13 | 14 |

Introducing Side View,
a new way to multitask with Firefox.

15 |
16 |
17 | 18 |

Side View lets you send websites to your Firefox sidebar.

19 |
20 |
21 | 22 |

By default, Side View displays mobile sites to keep everything looking good in a narrow window.

23 |
24 |
25 | 27 |

You can also right click to open links in Side View.

28 |
29 |
30 | 31 |
32 | 33 | 34 | 35 |
36 | 37 |
38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /addon/intro.js: -------------------------------------------------------------------------------- 1 | let foundSlides = 0; 2 | let currentSlide = 0; 3 | 4 | function selectSlide(num) { 5 | for (let i = 0; i < foundSlides; i++) { 6 | let el = document.querySelector(`#slide-${i}`); 7 | if (i === num) { 8 | el.classList.add("slide-displayed"); 9 | el.classList.remove("slide-hidden"); 10 | } else { 11 | el.classList.remove("slide-displayed"); 12 | el.classList.add("slide-hidden"); 13 | } 14 | } 15 | previous.disabled = num === 0; 16 | next.disabled = num === foundSlides - 1; 17 | } 18 | 19 | for (let el of document.querySelectorAll(".slide")) { 20 | let num = parseInt(el.id.replace(/[^\d]/g, ""), 10); 21 | foundSlides = Math.max(foundSlides + 1, num); 22 | } 23 | 24 | let previous = document.querySelector("#previous"); 25 | let next = document.querySelector("#next"); 26 | let done = document.querySelector("#done"); 27 | 28 | previous.addEventListener("click", () => { 29 | currentSlide = Math.max(0, currentSlide - 1); 30 | if (currentSlide === 0) { 31 | previous.classList.add("button-invisible"); 32 | } 33 | if (currentSlide === foundSlides - 2) { 34 | next.classList.remove("button-hidden"); 35 | done.classList.add("button-hidden"); 36 | } 37 | selectSlide(currentSlide); 38 | }); 39 | 40 | next.addEventListener("click", () => { 41 | currentSlide = Math.min(foundSlides - 1, currentSlide + 1); 42 | if (currentSlide === 1) { 43 | previous.classList.remove("button-invisible"); 44 | } 45 | if (currentSlide === foundSlides - 1) { 46 | next.classList.add("button-hidden"); 47 | done.classList.remove("button-hidden"); 48 | } 49 | selectSlide(currentSlide); 50 | }); 51 | 52 | done.addEventListener("click", () => { 53 | location.href = "popup.html"; 54 | }); 55 | 56 | selectSlide(currentSlide); 57 | -------------------------------------------------------------------------------- /addon/manifest.json.tmpl: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Side View", 4 | "version": "{{version}}", 5 | "description": "{{description}}", 6 | "icons": { 7 | "48": "side-view.png", 8 | "96": "side-view.png" 9 | }, 10 | "author": "{{{author}}}", 11 | "homepage_url": "{{{homepage}}}", 12 | "browser_specific_settings": { 13 | "gecko": { 14 | "id": "side-view@mozilla.org", 15 | "strict_min_version": "62.0" 16 | } 17 | }, 18 | "background": { 19 | "scripts": [ 20 | "build/buildSettings.js", 21 | "background.js" 22 | ] 23 | }, 24 | "browser_action": { 25 | "default_icon": "side-view.svg", 26 | "default_popup": "popup.html", 27 | "default_title": "Open Side View", 28 | "browser_style": true 29 | }, 30 | "sidebar_action": { 31 | "default_icon": "side-view.svg", 32 | "default_title": "Side View", 33 | "default_panel": "sidebar.html", 34 | "browser_style": false 35 | }, 36 | "page_action": { 37 | "default_icon": "side-view.svg", 38 | "default_title": "Open Side View", 39 | "show_matches": ["http://*/*", "https://*/*"], 40 | "browser_style": true 41 | }, 42 | "web_accessible_resources": [ 43 | ], 44 | "permissions": [ 45 | "activeTab", 46 | "tabs", 47 | "", 48 | "storage", 49 | "contextMenus", 50 | "webRequest", 51 | "webRequestBlocking", 52 | "bookmarks", 53 | "management" 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /addon/popup.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | font: message-box; 4 | overflow: hidden; 5 | } 6 | 7 | :root { 8 | --dark-theme-background-color: #4a4a4f; 9 | --dark-theme-highlight-color: #6d6d6f; 10 | --dark-theme-superhighlight-color: hsla(0, 0%, 80%, 0.45); 11 | --dark-theme-color: #fff; 12 | --dark-theme-links: #45a1ff; 13 | } 14 | 15 | * { 16 | box-sizing: border-box; 17 | } 18 | 19 | body { 20 | height: 100%; 21 | margin: 0; 22 | min-width: 320px; 23 | overflow: hidden !important; 24 | } 25 | 26 | #panel { 27 | background: #fff; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-between; 31 | max-width: 440px; 32 | min-width: 320px; 33 | width: 100%; 34 | } 35 | 36 | #panel, 37 | .tabs-section__title, 38 | .tab { 39 | font: menu; 40 | } 41 | 42 | .separator { 43 | background: hsla(210, 4%, 10%, 0.14); 44 | height: 1px; 45 | margin: 6px 0; 46 | } 47 | 48 | .tabs-wrapper { 49 | display: flex; 50 | flex-direction: column; 51 | flex: 1; 52 | } 53 | 54 | .tabs-section { 55 | display: flex; 56 | flex-direction: column; 57 | max-height: 300px; 58 | flex: 1 1; 59 | } 60 | 61 | .tabs-section__title { 62 | align-items: center; 63 | color: GrayText; 64 | display: flex; 65 | font-weight: normal; 66 | height: 24px; 67 | margin: 0; 68 | padding: 4px 12px; 69 | } 70 | 71 | .tabs-section:first-child .tabs-section__title { 72 | margin-top: 6px; 73 | } 74 | 75 | .tabs-section__list { 76 | list-style: none; 77 | margin: 0; 78 | padding: 0; 79 | } 80 | 81 | #open-tabs-list { 82 | flex: 1; 83 | overflow-y: auto; 84 | } 85 | 86 | .tab__parent { 87 | display: flex; 88 | width: 100%; 89 | background: #fff; 90 | height: 26.5px; 91 | } 92 | 93 | .tab { 94 | align-items: center; 95 | border: 0; 96 | width: 100%; 97 | background: transparent; 98 | font-weight: normal; 99 | padding-inline-start: 18px; 100 | padding: 4px 12px; 101 | display: flex; 102 | overflow: hidden; 103 | } 104 | 105 | .tab__parent:hover, 106 | .tab__parent:focus { 107 | background: #ededf0; 108 | } 109 | 110 | .tab__image, 111 | .tab__text { 112 | pointer-events: none; 113 | } 114 | 115 | .tab__image { 116 | background-size: 16px 16px; 117 | flex: 0 0 16px; 118 | height: 16px; 119 | margin-inline-end: 8px; 120 | } 121 | 122 | .tab__dismiss { 123 | border: 0; 124 | background: transparent; 125 | margin-top: 3.25px; 126 | margin-left: auto; 127 | margin-right: 10px; 128 | border-radius: 10%; 129 | opacity: 0; 130 | height: 20px; 131 | padding: 2px; 132 | } 133 | 134 | .tab__dismiss:hover, 135 | .tab__dismiss:focus { 136 | background: rgba(224, 224, 225, 0.9); 137 | } 138 | 139 | .tab__dismiss:hover, 140 | .tab__dismiss:focus, 141 | .tab:hover + .tab__dismiss, 142 | .tab:focus + .tab__dismiss, 143 | .tab__parent:hover .tab__dismiss, 144 | .tab__parent:focus .tab__dismiss { 145 | opacity: 100; 146 | } 147 | 148 | .tab__dismiss::after { 149 | content: url(images/close-16.svg); /* close symbol */ 150 | } 151 | 152 | /* this is a hack if, for any reason, a site does not 153 | supply a favicon */ 154 | 155 | .tab__image[style*=undefined] { 156 | background-image: url(images/globe.svg) !important; 157 | } 158 | 159 | .tab__text { 160 | margin-top: 1px; 161 | overflow: hidden; 162 | text-overflow: ellipsis; 163 | white-space: nowrap; 164 | } 165 | 166 | .panel-footer { 167 | border-top: 1px solid hsla(210, 4%, 10%, 0.14); 168 | display: flex; 169 | flex: 0 0 41px; 170 | margin: 6px 0 0; 171 | } 172 | 173 | .panel-footer.toggle-disabled { 174 | grid-template-columns: 1fr; 175 | } 176 | 177 | .mobile-toggle { 178 | border: 0; 179 | align-items: center; 180 | background: hsla(0, 0%, 80%, 0.3); 181 | color: rgb(26, 26, 26); 182 | cursor: default; 183 | display: flex; 184 | flex: 1; 185 | font: menu; 186 | justify-content: center; 187 | margin-bottom: 0; 188 | padding: 12px 4px; 189 | text-decoration: none; 190 | } 191 | 192 | .toggle-disabled .mobile-toggle { 193 | display: none; 194 | } 195 | 196 | .mobile-toggle:hover { 197 | background: hsla(0, 0%, 80%, 0.4); 198 | } 199 | 200 | #getting-started { 201 | display: none; 202 | padding: 1em; 203 | } 204 | 205 | #private-warning { 206 | background: #ededf0; 207 | border-bottom: 1px dashed #d70022; 208 | color: #d70022; 209 | display: flex; 210 | } 211 | 212 | #close-private-warning-copy { 213 | padding: 12px 0 12px 12px; 214 | } 215 | 216 | #close-private-warning { 217 | align-items: center; 218 | cursor: pointer; 219 | display: flex; 220 | flex: 0 0 24px; 221 | height: 24px; 222 | justify-content: center; 223 | } 224 | 225 | /* Dark theme */ 226 | 227 | #panel.dark-theme, 228 | #panel.dark-theme .tab, 229 | #panel.dark-theme .tab__parent, 230 | #panel.dark-theme .tab__dismiss { 231 | background-color: var(--dark-theme-background-color); 232 | color: var(--dark-theme-color); 233 | } 234 | 235 | #panel.dark-theme .tab__parent:hover, 236 | #panel.dark-theme .tab__parent:focus, 237 | #panel.dark-theme .tab__parent:hover > *, 238 | #panel.dark-theme .tab__parent:focus > *, 239 | #panel.dark-theme .separator { 240 | background-color: var(--dark-theme-highlight-color); 241 | } 242 | 243 | #panel.dark-theme .tab__dismiss:hover, 244 | #panel.dark-theme .tab__dismiss:focus { 245 | background-color: var(--dark-theme-background-color); 246 | } 247 | 248 | #panel.dark-theme .tab__dismiss::after { 249 | content: url(images/close-16-light.svg); 250 | } 251 | 252 | #panel.dark-theme .mobile-toggle { 253 | background-color: var(--dark-theme-superhighlight-color); 254 | color: var(--dark-theme-color); 255 | } 256 | 257 | #panel.dark-theme .mobile-toggle:hover { 258 | background-color: hsla(0, 0%, 80%, 0.6); 259 | } 260 | 261 | #recent-tabs[style$="none;"] + #open-tabs { 262 | margin-top: 6px; 263 | } 264 | 265 | #recent-tabs[style$="none;"] + #open-tabs .separator { 266 | display: none !important; 267 | } 268 | -------------------------------------------------------------------------------- /addon/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Popup 6 | 7 | 8 | 9 |
10 |
11 | To get started with Side View, open a website. 12 |
13 | 17 |
18 |
19 |

Recent

20 | 22 |
23 |
24 |
25 |

Current Tabs

26 | 28 |
29 |
30 |
31 | 32 |
33 |
34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /addon/popup.js: -------------------------------------------------------------------------------- 1 | /* globals buildSettings */ 2 | let isDesktop = false; 3 | let recentTabs = []; 4 | const rerenderEvents = ["onUpdated", "onRemoved", "onCreated", "onMoved", "onDetached", "onAttached"]; 5 | 6 | async function displayPage({url, title, favIconUrl}) { 7 | // Note this must be called in response to an event, so we can't call it in background.js: 8 | await browser.sidebarAction.open(); 9 | renderTabListLastRendered = {}; 10 | for (let eventName of rerenderEvents) { 11 | browser.tabs[eventName].removeListener(updateHome); 12 | } 13 | await browser.runtime.sendMessage({ 14 | type: "openUrl", 15 | url, 16 | title, 17 | favIconUrl, 18 | }); 19 | window.close(); 20 | } 21 | 22 | async function updateHome(event) { 23 | if (event) { 24 | // If this is called from an event, then often browser.windows.getCurrent() won't 25 | // be updated, and will return stale information, so we'll rerender a second time 26 | // very soon 27 | setTimeout(updateHome, 50); 28 | setTimeout(updateHome, 300); 29 | } 30 | const windowInfo = await browser.windows.getCurrent({populate: true}); 31 | let tabs = windowInfo.tabs.filter(tab => tab.url.startsWith("http")); 32 | if (tabs.length) { 33 | element("#open-tabs").style.display = "flex"; 34 | renderTabList(tabs, "#open-tabs-list", "existing-tab"); 35 | } else { 36 | element("#open-tabs").style.display = "none"; 37 | } 38 | if (recentTabs.length) { 39 | element("#recent-tabs").style.display = "flex"; 40 | renderTabList(recentTabs, "#recent-tabs-list", "recent-tab"); 41 | } else { 42 | element("#recent-tabs").style.display = "none"; 43 | } 44 | if (!tabs.length && !recentTabs.length) { 45 | element("#getting-started").style.display = "flex"; 46 | } else { 47 | element("#getting-started").style.display = "none"; 48 | } 49 | let onElement = element(".ask-for-desktop"); 50 | let offElement = element(".ask-for-mobile"); 51 | if (isDesktop) { 52 | [ onElement, offElement ] = [ offElement, onElement ]; 53 | } 54 | element(".mobile-toggle").title = onElement.textContent; 55 | onElement.style.display = ""; 56 | offElement.style.display = "none"; 57 | } 58 | 59 | let renderTabListLastRendered = {}; 60 | 61 | function _onTabClick(event, tabs, url, favIconUrl, index, title, eventLabel) { 62 | displayPage({ 63 | url, 64 | favIconUrl, 65 | title, 66 | }); 67 | } 68 | 69 | function renderTabList(tabs, containerSelector, eventLabel) { 70 | let renderedInfo = ""; 71 | const tabList = element(containerSelector); 72 | const newTabList = tabList.cloneNode(); 73 | tabs.forEach((tab, index) => { 74 | let li = document.createElement("li"); 75 | let parent = document.createElement("div"); 76 | let image = document.createElement("span"); 77 | let text = document.createElement("span"); 78 | let dismiss = document.createElement("button"); 79 | parent.classList.add("tab__parent"); 80 | image.classList.add("tab__image"); 81 | text.classList.add("tab__text"); 82 | dismiss.classList.add("tab__dismiss"); 83 | dismiss.setAttribute("aria-label", "close button"); 84 | dismiss.setAttribute("title", "Remove Tab from Recent List"); 85 | let title = tab.title; 86 | let url = tab.url; 87 | let favIconUrl = null; 88 | if ("favIconUrl" in tab && tab.favIconUrl) { 89 | favIconUrl = tab.favIconUrl; 90 | image.style.backgroundImage = `url(${favIconUrl})`; 91 | } 92 | renderedInfo += favIconUrl + " "; 93 | let anchor = document.createElement("button"); 94 | renderedInfo += url + " "; 95 | anchor.classList.add("tab"); 96 | text.textContent = title; 97 | renderedInfo += title + "\n"; 98 | anchor.addEventListener("click", (event) => 99 | _onTabClick(event, tabs, url, favIconUrl, index, title, eventLabel)); 100 | parent.addEventListener("click", (event) => 101 | _onTabClick(event, tabs, url, favIconUrl, index, title, eventLabel)); 102 | // Only add the dismiss button if its a recent tab 103 | if (eventLabel === "recent-tab") { 104 | dismiss.addEventListener("click", async (event) => { 105 | event.stopPropagation(); // prevent the selection of tab 106 | await browser.runtime.sendMessage({ 107 | type: "dismissTab", 108 | index, 109 | }); 110 | }); 111 | } 112 | anchor.prepend(image); 113 | anchor.appendChild(text); 114 | parent.appendChild(anchor); 115 | if (eventLabel === "recent-tab") { 116 | parent.appendChild(dismiss); 117 | } 118 | li.appendChild(parent); 119 | newTabList.appendChild(li); 120 | }); 121 | if (renderedInfo !== renderTabListLastRendered[containerSelector]) { 122 | tabList.replaceWith(newTabList); 123 | renderTabListLastRendered[containerSelector] = renderedInfo; 124 | } 125 | } 126 | 127 | function element(selector) { 128 | return document.querySelector(selector); 129 | } 130 | 131 | element(".mobile-toggle").addEventListener("click", async () => { 132 | await browser.sidebarAction.open(); 133 | await browser.runtime.sendMessage({ 134 | type: "toggleDesktop", 135 | }); 136 | }); 137 | 138 | element("#close-private-warning").addEventListener("click", async () => { 139 | await browser.runtime.sendMessage({ 140 | type: "turnOffPrivateWarning", 141 | }); 142 | element("#private-warning").style.display = "none"; 143 | }); 144 | 145 | function loadCachedRecentTabs() { 146 | let value = localStorage.getItem("recentTabs") || "[]"; 147 | value = JSON.parse(value); 148 | return value; 149 | } 150 | 151 | function cacheRecentTabs(value) { 152 | localStorage.setItem("recentTabs", JSON.stringify(value)); 153 | } 154 | 155 | function applyDarkTheme() { 156 | document.body.style.background = "#4a4a4f"; 157 | document.body.style.color = "#fff"; 158 | document.querySelector("#panel").classList.add("dark-theme"); 159 | } 160 | 161 | async function checkForDark() { 162 | browser.management.getAll().then((extensions) => { 163 | for (let extension of extensions) { 164 | // The user has the default dark theme enabled 165 | if (extension.id === 166 | "firefox-compact-dark@mozilla.org@personas.mozilla.org" 167 | && extension.enabled) { 168 | applyDarkTheme(); 169 | } 170 | } 171 | }); 172 | } 173 | 174 | async function init() { 175 | document.addEventListener("contextmenu", event => event.preventDefault()); 176 | 177 | browser.runtime.onMessage.addListener((message) => { 178 | if (message.type === "updateRecentTabs") { 179 | recentTabs = message.recentTabs; 180 | updateHome(); 181 | cacheRecentTabs(recentTabs); 182 | } else if (message.type === "isDesktop") { 183 | isDesktop = message.isDesktop; 184 | updateHome(); 185 | } else if (["setDesktop", "sidebarOpenedPage", "sidebarDisplayedHome", "getRecentTabs"].includes(message.type)) { 186 | // These intended to go to the backgrond and can be ignored here 187 | } else { 188 | console.error("Got unexpected message:", message); 189 | } 190 | }); 191 | 192 | recentTabs = loadCachedRecentTabs(); 193 | updateHome(); 194 | 195 | let info = await browser.runtime.sendMessage({ 196 | type: "getRecentAndDesktop", 197 | }); 198 | recentTabs = info.recentTabs; 199 | isDesktop = info.isDesktop; 200 | if (info.incognito && !info.hasSeenPrivateWarning) { 201 | element("#private-warning").style.display = ""; 202 | } 203 | updateHome(); 204 | 205 | // Listen for tab changes to update while popup is still open 206 | for (let eventName of rerenderEvents) { 207 | browser.tabs[eventName].addListener(updateHome); 208 | } 209 | checkForDark(); 210 | } 211 | 212 | init(); 213 | -------------------------------------------------------------------------------- /addon/side-view-badged.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addon/side-view-onboarding.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /addon/side-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/side-view/433e591750eca2468e7f893b5caaa24f2993ff98/addon/side-view.png -------------------------------------------------------------------------------- /addon/side-view.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /addon/sidebar.css: -------------------------------------------------------------------------------- 1 | body, 2 | html { 3 | font: message-box; 4 | font-size: 15px; 5 | overflow: hidden; 6 | color: rgb(12, 12, 13); 7 | } 8 | 9 | :root { 10 | --dark-theme-background-color: #4a4a4f; 11 | --dark-theme-highlight-color: #6d6d6f; 12 | --dark-theme-color: #fff; 13 | --dark-theme-links: #45a1ff; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | text-align: center; 19 | } 20 | 21 | body { 22 | margin: 0; 23 | } 24 | 25 | #home-panel { 26 | display: none; 27 | } 28 | 29 | .page { 30 | background: #f9f9fa; 31 | min-height: 100vh; 32 | padding: 40px 28px; 33 | } 34 | 35 | .title { 36 | font-size: 1.46em; 37 | font-weight: 300; 38 | line-height: 1.3em; 39 | margin: 0; 40 | } 41 | 42 | .subtitle { 43 | font-size: 1em; 44 | font-weight: 400; 45 | margin: 6px 0 24px; 46 | } 47 | 48 | .subtitle a { 49 | color: rgb(12, 12, 13); 50 | } 51 | 52 | .instructions { 53 | list-style: none; 54 | margin: 0; 55 | padding: 0; 56 | } 57 | 58 | #landing-panel { 59 | align-items: center; 60 | display: flex; 61 | flex-direction: column; 62 | justify-content: center; 63 | } 64 | 65 | .graphic { 66 | align-items: center; 67 | display: flex; 68 | flex-direction: column; 69 | margin-bottom: 16px; 70 | width: 71vw; 71 | max-width: 284px; 72 | height: 52vw; 73 | max-height: 206px; 74 | } 75 | 76 | .graphic__overlay { 77 | position: relative; 78 | max-width: 284px; 79 | width: 100%; 80 | height: 61vw; 81 | max-height: 174px; 82 | margin-bottom: 9.2%; 83 | } 84 | 85 | .graphic__clouds { 86 | max-width: 284px; 87 | width: 100%; 88 | max-height: 174px; 89 | height: 100%; 90 | background: url(images/in-content-clouds.png) no-repeat center; 91 | background-size: contain; 92 | position: absolute; 93 | } 94 | 95 | .graphic__browser { 96 | animation: fade-in-down forwards 1000ms cubic-bezier(0.22, 0.92, 0.62, 1); 97 | animation-delay: 100ms; 98 | background: url(images/in-content-icon.png) no-repeat center; 99 | background-size: contain; 100 | max-width: 284px; 101 | width: 100%; 102 | height: 100%; 103 | opacity: 0; 104 | position: absolute; 105 | transform: translate3d(0, -72px, 0); 106 | } 107 | 108 | .graphic__shadow { 109 | animation: fade-in-scale forwards 1000ms cubic-bezier(0.22, 0.92, 0.62, 1); 110 | animation-delay: 250ms; 111 | max-width: 170px; 112 | width: 61.2%; 113 | height: 16px; 114 | border-radius: 50%; 115 | background: #ededf0; 116 | opacity: 0; 117 | transform: scale(1.3); 118 | } 119 | 120 | @keyframes fade-in-down { 121 | 0% { 122 | opacity: 0; 123 | transform: translate3d(0, -72px, 0); 124 | } 125 | 126 | 100% { 127 | opacity: 1; 128 | transform: translate3d(0, 0, 0); 129 | } 130 | } 131 | 132 | @keyframes fade-in-scale { 133 | 0% { 134 | opacity: 0; 135 | transform: scale(1.3); 136 | } 137 | 138 | 100% { 139 | opacity: 1; 140 | transform: scale(1); 141 | } 142 | } 143 | 144 | .default-link { 145 | appearance: none; 146 | background-color: rgba(0, 0, 0, 0); 147 | border: 0; 148 | color: rgb(10, 141, 255); 149 | cursor: pointer; 150 | font-size: 15px; 151 | margin: 6px 0; 152 | padding: 0; 153 | text-decoration: none; 154 | transition: color 100ms; 155 | } 156 | 157 | .subtitle .default-link { 158 | margin: 0; 159 | padding: 0; 160 | } 161 | 162 | .default-link:hover, 163 | .default-link:focus { 164 | color: rgb(0, 96, 223); 165 | text-decoration: underline; 166 | } 167 | 168 | /* Dark theme */ 169 | 170 | .page.dark-theme { 171 | color: var(--dark-theme-color); 172 | background: var(--dark-theme-background-color); 173 | } 174 | 175 | .page.dark-theme button:hover, 176 | .page.dark-theme button:focus, 177 | .page.dark-theme .graphic__shadow { 178 | background: var(--dark-theme-highlight-color); 179 | background-color: transparent; 180 | } 181 | 182 | .page.dark-theme .default-link, 183 | .page.dark-theme .default-link:hover, 184 | .page.dark-theme .default-link:focus { 185 | color: var(--dark-theme-links); 186 | } 187 | 188 | .page.dark-theme .graphic__clouds { 189 | background: url(images/in-content-clouds-dark.png); 190 | } 191 | -------------------------------------------------------------------------------- /addon/sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sidebar 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

Welcome to Side View

18 | 19 |

20 | Watch a tutorial 21 |

22 |
23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /addon/sidebar.js: -------------------------------------------------------------------------------- 1 | /* globals buildSettings */ 2 | 3 | function element(selector) { 4 | return document.querySelector(selector); 5 | } 6 | 7 | function applyDarkTheme() { 8 | document.querySelector(".page").classList.add("dark-theme"); 9 | } 10 | 11 | async function checkForDark() { 12 | browser.management.getAll().then((extensions) => { 13 | for (let extension of extensions) { 14 | // The user has the default dark theme enabled 15 | if (extension.id === 16 | "firefox-compact-dark@mozilla.org@personas.mozilla.org" 17 | && extension.enabled) { 18 | applyDarkTheme(); 19 | } 20 | } 21 | }); 22 | } 23 | 24 | async function init() { 25 | element("#watch-tutorial").onclick = () => { 26 | window.open("https://youtu.be/no6D_B4wgo8"); 27 | }; 28 | 29 | checkForDark(); 30 | browser.management.onEnabled.addListener((info) => { 31 | checkForDark(); 32 | }); 33 | } 34 | 35 | init(); 36 | -------------------------------------------------------------------------------- /docs/release.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | ## addons.mozilla.org 4 | 5 | To build for AMO: 6 | 7 | ```sh 8 | npm run package 9 | # XPI is in ./addon-amo.xpi 10 | ``` 11 | 12 | Then upload manually via the [web interface](https://addons.mozilla.org/en-US/developers/addon/side-view/edit) (TODO: use web-ext to further automate the release). 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "side-view", 3 | "description": "Open a mobile view of a page in the sidebar", 4 | "version": "0.6.0", 5 | "author": "Mozilla (https://mozilla.org/)", 6 | "bugs": { 7 | "url": "https://github.com/mozilla/side-view/issues" 8 | }, 9 | "devDependencies": { 10 | "addons-linter": "^7.3.0", 11 | "eslint": "^5.5.0", 12 | "eslint-plugin-mozilla": "^0.15.4", 13 | "eslint-plugin-no-unsanitized": "^3.0.2", 14 | "mustache": "^2.3.2", 15 | "npm-run-all": "4.1.5", 16 | "stylelint": "^9.5.0", 17 | "stylelint-config-standard": "^18.2.0", 18 | "web-ext": "^2.9.1" 19 | }, 20 | "homepage": "https://github.com/mozilla/side-view/", 21 | "license": "MPL-2.0", 22 | "private": true, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/mozilla/side-view.git" 26 | }, 27 | "scripts": { 28 | "start": "npm-run-all build run", 29 | "lint": "npm-run-all lint:*", 30 | "lint:addon": "npm run package && addons-linter ./addon.xpi -o text --self-hosted", 31 | "lint:js": "eslint .", 32 | "lint:styles": "stylelint ./addon/*.css", 33 | "build": "npm-run-all build:*", 34 | "build:manifest": "node -e 'let input = JSON.parse(fs.readFileSync(\"package.json\")); input.version = input.version.slice(0, -1) + Math.floor((Date.now() - new Date(new Date().getFullYear().toString()).getTime()) / 3600000); Object.assign(input, process.env); console.log(JSON.stringify(input))' | mustache - addon/manifest.json.tmpl > addon/manifest.json", 35 | "build:buildSettings": "mkdir -p addon/build/ && node -e 'console.log(JSON.stringify(process.env))' | mustache - addon/buildSettings.js.tmpl > addon/build/buildSettings.js", 36 | "build:web-ext": "web-ext build --source-dir=addon --overwrite-dest --ignore-files '*.tmpl'", 37 | "package": "npm run build && cp web-ext-artifacts/`ls -t1 web-ext-artifacts | head -n 1` addon.xpi", 38 | "run": "mkdir -p ./Profile && web-ext run --source-dir=addon -p ./Profile --browser-console --keep-profile-changes -f nightly", 39 | "test": "npm run lint" 40 | } 41 | } 42 | --------------------------------------------------------------------------------