├── CONTRIBUTING.md ├── LICENSE ├── README.md └── src ├── images ├── tabber_128.png ├── tabber_16.png ├── tabber_32.png ├── tabber_48.png ├── tabber_green_19.png ├── tabber_green_38.png ├── tabber_red_19.png ├── tabber_red_38.png ├── tabber_ui.png ├── tabber_yellow_19.png └── tabber_yellow_38.png ├── js ├── externs.js ├── interns.js ├── popup.js ├── sync_phase_handler.js ├── tabber.js ├── tabber_session.js └── tabs.js ├── manifest.json └── static └── popup.html /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution; 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chrome Tabber Project 2 | 3 | This repository contains the open source project Chrome Tabber. 4 | 5 | This project uses the Apache license. 6 | 7 | ## What does Chrome Tabber Do? 8 | 9 | Chrome Tabber allows you to share your browser tabs across devices very simply, 10 | using your Google account. 11 | 12 | By default, when you open the Chrome browser, the tabs are initialized to the 13 | same state as when you closed the browser *on that device.* 14 | 15 | Although there is a setting that lets you change that behavior, you cannot 16 | easily restore tabs from another device. The official way to do that in Chrome 17 | is to save your current tabs as a group bookmark (*"Bookmark Open Pages..."*) 18 | and then open that bookmark on another device. 19 | 20 | Chrome Tabber lets you instantly sync your tabs across Chrome browsers on any 21 | device logged into your Google account. It provides several modes of operation 22 | so you can customize this behavior to fit your needs. 23 | 24 | ## How to use Chrome Tabber 25 | 26 | After installing Chrome Tabber it will display a little icon in Chrome's 27 | extension icon area (typically the upper right of the browser). Click this icon 28 | in order see Chrome Tabber's popup UI. 29 | 30 | On Tabber's popup, you can see status, perform manual syncing, or change the 31 | mode of operation. The mode of operation is persistent, and applies to the 32 | *current device* only. That means you can set different modes on different 33 | devices (which is a useful and typical way to set up your devices). 34 | 35 | ### Chrome Tabber Modes 36 | 37 | * Manual: Just like it sounds, in order to save or load tabs, you must click a 38 | button on Tabber's popup UI. 39 | 40 | * Startup Only: Your saved tabs are loaded only when the browser is opened. 41 | 42 | * Auto-Save: Like *Startup Only* but any changes you make on this device are 43 | saved for sharing to other devices. 44 | 45 | * Fully Automatic: Like Auto-Save but Chrome Tabber will also detect changes 46 | made on other devices and automatically load them. 47 | 48 | By setting various modes on your devices, you can support different use cases. 49 | Here are some common scenarios: 50 | 51 | QUESTION: I have one main device and multiple secondary devices. How do I set up 52 | Chrome Tabber so that whenever I start Chrome on a secondary device, it opens 53 | with the same tabs I last used on my main device? 54 | 55 | ANSWER: Set your main device to *Auto-Save* and secondary devices to *Startup 56 | Only.* With this setup, subsequent tabs that you open or close on your secondary 57 | devices do not get reflected back to your main device. 58 | 59 | QUESTION: I have several devices I use. How do I make it so they all show the 60 | same tabs all the time? 61 | 62 | ANSWER: Set all devices to *Fully Automatic* 63 | 64 | QUESTION: I use one device almost all the time, but occasionally I do some work 65 | on another device. How do I make it so that I can move from device to device, 66 | and always 'pick up where I left off?' 67 | 68 | ANSWER: There are several ways to do this: 69 | 70 | 1. Set all devices to *Fully Automatic* 71 | 72 | 1. Set main device to *Fully Automatic* and secondary devices to *Auto-Save* 73 | 74 | 1. Use manual save and load operations to 'move' your work whenever you like. 75 | 76 | Best Practice: Set main device to *Fully Automatic* and secondary devices to 77 | *Startup Only.* Once you are done working on the secondary device, manually 78 | click the "Save the Tabs I have Now" button. 79 | 80 | # NOTE 81 | This is not an official Google product. 82 | -------------------------------------------------------------------------------- /src/images/tabber_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_128.png -------------------------------------------------------------------------------- /src/images/tabber_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_16.png -------------------------------------------------------------------------------- /src/images/tabber_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_32.png -------------------------------------------------------------------------------- /src/images/tabber_48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_48.png -------------------------------------------------------------------------------- /src/images/tabber_green_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_green_19.png -------------------------------------------------------------------------------- /src/images/tabber_green_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_green_38.png -------------------------------------------------------------------------------- /src/images/tabber_red_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_red_19.png -------------------------------------------------------------------------------- /src/images/tabber_red_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_red_38.png -------------------------------------------------------------------------------- /src/images/tabber_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_ui.png -------------------------------------------------------------------------------- /src/images/tabber_yellow_19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_yellow_19.png -------------------------------------------------------------------------------- /src/images/tabber_yellow_38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/chrome-tabber/f0b4529e4550fd094d79c5c6e1716131119eee21/src/images/tabber_yellow_38.png -------------------------------------------------------------------------------- /src/js/externs.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Define the Tabber interface which provides the external tabber access. 19 | * All external Tabber access goes through a singleton Tabber object which 20 | * implements this interface. 21 | * @fileoverview 22 | */ 23 | 24 | /** 25 | * Define the actual Tabber API interface. 26 | * @interface 27 | */ 28 | function TabberApi() {} 29 | 30 | /** 31 | * @type TabberApi.type.mode 32 | */ 33 | TabberApi.prototype.mode; 34 | 35 | /** 36 | * @type TabberApi.type.state 37 | */ 38 | TabberApi.prototype.state; 39 | 40 | /** 41 | * Save the current local tabs. 42 | */ 43 | TabberApi.prototype.saveLocalToRemote = function() {}; 44 | 45 | /** 46 | * Set the browser tabs to match the saved set. 47 | */ 48 | TabberApi.prototype.syncBrowserFromRemote = function() {}; 49 | 50 | /** 51 | * Setting Tabber options. 52 | * @param {TabberApi.type.config} config - The desired config option values. 53 | * @return {undefined} 54 | */ 55 | TabberApi.prototype.setOptions = function(config) {}; 56 | 57 | /** 58 | * Getting Tabber status. 59 | * @return {TabberApi.type.status} 60 | */ 61 | TabberApi.prototype.getStatus = function() {}; 62 | 63 | /** 64 | * Define type property and associated TabberApi types. 65 | */ 66 | TabberApi.type = function() {}; 67 | 68 | /** 69 | * Define information types. 70 | * @typedef {{ 71 | * OK: number, 72 | * WARN: number, 73 | * ERR: number 74 | * }} 75 | */ 76 | TabberApi.type.state; 77 | 78 | /** 79 | * Define operational modes. 80 | * @typedef {{ 81 | * AUTOSYNC: string, 82 | * AUTOSTART: string, 83 | * AUTOSAVE: string, 84 | * MANUAL: string 85 | * }} 86 | */ 87 | TabberApi.type.mode; 88 | 89 | /** 90 | * Define operational configuration options. 91 | * @typedef {{ 92 | * mode: (undefined|string), 93 | * debug: (undefined|boolean) 94 | * }} 95 | */ 96 | TabberApi.type.config; 97 | 98 | /** 99 | * Define information types. 100 | * @typedef {{ 101 | * state: string, 102 | * msg: string 103 | * }} 104 | */ 105 | TabberApi.type.sync_state; 106 | 107 | /** 108 | * Define information types. 109 | * @typedef {{ 110 | * options: TabberApi.type.config, 111 | * sync: TabberApi.type.sync_state, 112 | * remote_time: string 113 | * }} 114 | */ 115 | TabberApi.type.status; 116 | 117 | -------------------------------------------------------------------------------- /src/js/interns.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * Define the Tabber internal types and classes which are not exposed to the 19 | * popup UI, but still need to be name-protected or type checked inside Tabber. 20 | * @fileoverview 21 | */ 22 | 23 | /** 24 | * Define the TabberInt object which defines the internal types and methods. 25 | * @interface 26 | */ 27 | function TabberInt() {} 28 | 29 | /** 30 | * Define Tab object fields we care about. The only required fields are the 31 | * url and the index (which are also the only ones that can be specified to 32 | * chrome when creating a new tab). 33 | * @typedef {{ 34 | * url: string, 35 | * index: string, 36 | * id: (string|undefined), 37 | * windowId: (number|undefined), 38 | * active: (boolean|undefined), 39 | * title: (string|undefined) 40 | * }} 41 | */ 42 | TabberInt.type.Tab; 43 | 44 | /** 45 | * Define tabdiff object fields. 46 | * @typedef {{ 47 | * major: string, 48 | * minor: string, 49 | * err: boolean 50 | * }} 51 | */ 52 | TabberInt.type.TabDiff; 53 | 54 | /** 55 | * Define the TabberSession object which holds saved tabs data. 56 | * @interface 57 | */ 58 | TabberInt.TabberSession = function() {}; 59 | 60 | /** @type {string} */ 61 | TabberInt.TabberSession.prototype.description; 62 | 63 | /** @type {number} */ 64 | TabberInt.TabberSession.prototype.generation; 65 | 66 | /** @type {Object|number} */ 67 | TabberInt.TabberSession.prototype.numtabs; 68 | 69 | /** @type {Array} */ 70 | TabberInt.TabberSession.prototype.tabs; 71 | 72 | /** @type {number|boolean} */ 73 | TabberInt.TabberSession.prototype.updateTime; 74 | 75 | /** 76 | * Helper to print session info to console. 77 | * @param {string} label - Label to tag the output with. 78 | */ 79 | TabberInt.TabberSession.prototype.printSession = function(label) {}; 80 | 81 | /** 82 | * Return the last touch time as a user string. If this session has not yet 83 | * been initialized, it returns an empty string. 84 | * @return {string} 85 | */ 86 | TabberInt.TabberSession.prototype.getTimeString = function() {}; 87 | 88 | -------------------------------------------------------------------------------- /src/js/popup.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // The popup script provides the UI for the current Tabber state 18 | 19 | /** 20 | * Gets Tabber access from the background page. 21 | * @return {TabberApi} 22 | */ 23 | function getTabber() { 24 | var bgPage = chrome.extension.getBackgroundPage(); 25 | if (!bgPage) { 26 | // Really shouldn't happen 27 | alert('Chrome error accessing background page!\n' + 28 | 'Re-install Tabber and/or restart Chrome!'); 29 | } 30 | /** 31 | * Get the background page's Tabber singleton. 32 | * @return {TabberApi} 33 | */ 34 | var tabber = bgPage['Tabber']; 35 | if (!tabber) { 36 | // Really shouldn't happen 37 | alert('Tabber not found! Reload Tabber extension and try again.'); 38 | } 39 | return tabber; 40 | } 41 | 42 | /** 43 | * UI event handler to grab the current tabs and save them. 44 | */ 45 | function onSave() { 46 | console.log('Trying to do a Tabber.saveLocalToRemote'); 47 | getTabber()['saveLocalToRemote'](); 48 | console.log('Tabber.saveLocalToRemote call done'); 49 | // Dismiss our popup. 50 | window.close(); 51 | } 52 | 53 | /** 54 | * UI event handler to grab the saved tabs and make the browser match. 55 | */ 56 | function onRestore() { 57 | console.log('Trying to do a Tabber.syncBrowserFromRemote'); 58 | getTabber()['syncBrowserFromRemote'](); 59 | console.log('Tabber.syncBrowserFromRemote call done'); 60 | // Dismiss our popup. 61 | window.close(); 62 | } 63 | 64 | /** 65 | * UI event handler to merge the saved tabs with the current browser tabs. 66 | */ 67 | function onMerge() { 68 | console.log('Trying to do a Tabber.mergeBrowserWithRemote'); 69 | // TODO: enable this when ready 70 | // getTabber()['mergeBrowserWithRemote'](); 71 | // Dismiss our popup. 72 | window.close(); 73 | } 74 | 75 | /** 76 | * Swap the tabs in the saved session with current browser tabs 77 | */ 78 | function onSwap() { 79 | console.log('Trying to do a Tabber.swapBrowserWithRemote'); 80 | // TODO: enable this when ready 81 | // getTabber()['mergeBrowserWithRemote'](); 82 | // Dismiss our popup. 83 | window.close(); 84 | } 85 | 86 | /** 87 | * Event handler to set the Tabber mode from the UI. 88 | */ 89 | function onManualMode() { 90 | var tbr = getTabber(); 91 | tbr.setOptions({mode: tbr.mode.MANUAL}); 92 | } 93 | 94 | /** 95 | * Event handler to set the Tabber mode from the UI. 96 | */ 97 | function onAutostartMode() { 98 | var tbr = getTabber(); 99 | tbr.setOptions({mode: tbr.mode.AUTOSTART}); 100 | } 101 | 102 | /** 103 | * Event handler to set the Tabber mode from the UI. 104 | */ 105 | function onAutosaveMode() { 106 | var tbr = getTabber(); 107 | tbr.setOptions({mode: tbr.mode.AUTOSAVE}); 108 | } 109 | 110 | /** 111 | * Event handler to set the Tabber mode from the UI. 112 | */ 113 | function onAutosyncMode() { 114 | var tbr = getTabber(); 115 | tbr.setOptions({mode: tbr.mode.AUTOSYNC}); 116 | } 117 | 118 | /** 119 | * Event handler that set Tabber debug mode from UI. 120 | */ 121 | function onDebugMode() { 122 | // Get new debug mode state 123 | /** type {boolean} */ 124 | var state = document.getElementById('debug_mode').checked; 125 | getTabber().setOptions({debug: state}); 126 | } 127 | 128 | // Once the popup page is loaded, finish init 129 | document.addEventListener('DOMContentLoaded', function() { 130 | /** 131 | * @type {TabberApi} 132 | */ 133 | var tbr = getTabber(); 134 | var status = tbr.getStatus(); 135 | // init our UI 136 | if (status.options.mode == 'autosync') { 137 | document.getElementById('autosync').checked = true; 138 | } else if (status.options.mode == 'autosave') { 139 | document.getElementById('autosave').checked = true; 140 | } else if (status.options.mode == 'autostart') { 141 | document.getElementById('autostart').checked = true; 142 | } else { 143 | document.getElementById('manual').checked = true; 144 | } 145 | document.getElementById('debug_mode').checked = status.options.debug; 146 | 147 | // set our status message 148 | var diff = document.getElementById('diff'); 149 | // Show Tabber sync message 150 | diff.textContent = status.sync.msg; 151 | // set text field color by status 152 | if (status.sync.state == tbr.state.OK) { 153 | diff.style.backgroundColor = 'lightgreen'; 154 | } else if (status.sync.state == tbr.state.WARN) { 155 | diff.style.backgroundColor = 'yellow'; 156 | } else { 157 | diff.style.backgroundColor = 'salmon'; 158 | } 159 | // If we have a remote session, show its' timestamp. 160 | // Also, the ability to restore is based on a valid remote session. 161 | if (status['remote_time']) { 162 | document.getElementById('timestamp').textContent = 'Saved session from: ' + 163 | status['remote_time']; 164 | document.getElementById('restore').disabled = false; 165 | } else { 166 | document.getElementById('timestamp').textContent = 'No saved session available'; 167 | document.getElementById('restore').disabled = true; 168 | } 169 | 170 | // TODO: add merge capability when supported by Tabber 171 | document.getElementById('merge').disabled = true; 172 | 173 | // TODO: add swap capability when supported by Tabber 174 | document.getElementById('swap').disabled = true; 175 | 176 | // TODO: enable full autosync capability when better tested 177 | // document.getElementById('autosync').disabled = true; 178 | 179 | // hook our action functions to UI elements 180 | document.getElementById('restore').addEventListener('click', onRestore); 181 | document.getElementById('save').addEventListener('click', onSave); 182 | document.getElementById('merge').addEventListener('click', onMerge); 183 | document.getElementById('swap').addEventListener('click', onSwap); 184 | document.getElementById('manual').addEventListener('click', onManualMode); 185 | document.getElementById('autostart').addEventListener('click', onAutostartMode); 186 | document.getElementById('autosave').addEventListener('click', onAutosaveMode); 187 | document.getElementById('autosync').addEventListener('click', onAutosyncMode); 188 | document.getElementById('debug_mode').addEventListener('click', onDebugMode); 189 | }); 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /src/js/sync_phase_handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview 19 | * sync_phase_handler.js - Provides a management mechanism for serializing a 20 | * sequence of asynchronous chrome API calls, and kicking off the subsequent 21 | * processing when the last call is complete. 22 | */ 23 | 24 | goog.provide('TabberInternal.SyncPhaseHandler'); 25 | 26 | /** 27 | * Private globals 28 | */ 29 | 30 | /** 31 | * @private 32 | * @type {number} 33 | */ 34 | var change_count_; 35 | 36 | /** 37 | * @private 38 | * @type {boolean} 39 | */ 40 | var pending_changes; 41 | 42 | /** 43 | * @private 44 | * @type {function()} 45 | */ 46 | var completionCallback; 47 | 48 | /** 49 | * @private 50 | * @type {?function(*, Array<*>)} 51 | */ 52 | var stepCallback; 53 | 54 | /** 55 | * @private 56 | * @type {?*} 57 | */ 58 | var stepCallbackContext; 59 | 60 | 61 | /** 62 | * Initialize the state of the SyncPhaseHandler for a new phase. 63 | * @param {function()} callback - The function to call when all chrome callbacks 64 | * are complete. 65 | */ 66 | TabberInternal.SyncPhaseHandler.initHandler = function(callback) { 67 | change_count_ = 0; 68 | pending_changes = true; 69 | completionCallback = callback; 70 | }; 71 | 72 | /** 73 | * Register a callback for the next doStep completion. 74 | * @param {function(*, Array<*>)} callback - called after next doStep. 75 | * @param {*} ctx - Context passed to callback function. 76 | * @return {undefined} 77 | */ 78 | TabberInternal.SyncPhaseHandler.setDoStepCallback = function(callback, ctx) { 79 | // consoleLog('setDoStepCallback callback = ' + callback); 80 | // consoleLog('setDoStepCallback ctx = ' + JSON.stringify(ctx)); 81 | stepCallback = callback; 82 | stepCallbackContext = ctx; 83 | }; 84 | 85 | /** 86 | * Submit a phase step function call. Func must be of the form: 87 | * function func(arg1, arg2, ..., callback); 88 | * @param {!function(...)} chromeFunction - Required chrome function to call. 89 | * @param {...} arglist - Zero to many arbitrary optional args. 90 | * @return {undefined} 91 | */ 92 | TabberInternal.SyncPhaseHandler.doStep = function(chromeFunction, arglist) { 93 | // consoleLog('doStep called w/' + arguments.length + ' args'); 94 | // Convert arguments to an args Array so we can manipulate it 95 | var args = (arguments.length === 1 ? [arguments[0]] : 96 | Array.apply(null, arguments)); 97 | // The function is the first argument 98 | var func = args.shift(); 99 | // The function is expected to take a callback as the last argument 100 | args.push(TabberInternal.SyncPhaseHandler.changeDone); 101 | // Increment for our pending func callback 102 | change_count_++; 103 | // call the func 104 | /* 105 | for (var a = 0; a < args.length; a++) { 106 | if (a == args.length - 1) { 107 | consoleLog('chrome func arg ' + a + ': ' + args[a]); 108 | } else { 109 | consoleLog('chrome func arg ' + a + ': ' + JSON.stringify(args[a])); 110 | } 111 | } 112 | */ 113 | chromeFunction.apply(null, args); 114 | }; 115 | 116 | 117 | /** 118 | * Called by Chrome as API callback. 119 | * @param {*} arglist - Whatever chrome passes to the callback. 120 | * @return {undefined} 121 | */ 122 | TabberInternal.SyncPhaseHandler.changeDone = function(arglist) { 123 | // consoleLog('changeDone gets ' + arguments.length + ' args, first: ' + 124 | // JSON.stringify(arglist)); 125 | 126 | // First call the step callback if applicable. 127 | if (stepCallback) { 128 | // consoleLog('On changeDone, calling ' + stepCallback); 129 | // Convert chrome callback arguments to an Array. 130 | var args = (arguments.length === 1 ? [arguments[0]] : 131 | Array.apply(null, arguments)); 132 | // Save the callback info locally, so we can clear it before the call. 133 | var loc_stepCallback = stepCallback; 134 | var loc_stepCallbackContext = stepCallbackContext; 135 | // Reset the global callback info. 136 | stepCallback = null; 137 | stepCallbackContext = null; 138 | // Pass it to the callback along with his preset context. 139 | loc_stepCallback(loc_stepCallbackContext, args); 140 | } 141 | /* 142 | consoleLog('On changeDone, SyncPhaseHandle.change_count_=' + 143 | change_count_); 144 | consoleLog('On changeDone, SyncPhaseHandle.pending_changes=' + 145 | pending_changes); 146 | */ 147 | // decrement change count (should never go negative) 148 | if (change_count_ > 0) { 149 | change_count_--; 150 | } 151 | // if it reached zero and no more changes are pending 152 | if ((change_count_ == 0) && !pending_changes) { 153 | // then call the completion callback 154 | // consoleLog('SyncPhaseHandler calling completion'); 155 | completionCallback(); 156 | } 157 | }; 158 | 159 | 160 | /** 161 | * Finalize this phase. Called after all steps are submitted. 162 | * @return {undefined} 163 | */ 164 | TabberInternal.SyncPhaseHandler.finalize = function() { 165 | // consoleLog('SyncPhaseHandler.finalize called (cc=' + change_count_); 166 | // The last change completion will move to the next phase 167 | pending_changes = false; 168 | // If last change already completed, we can move on here 169 | if (change_count_ == 0) { 170 | TabberInternal.SyncPhaseHandler.changeDone(null); 171 | } 172 | }; 173 | 174 | -------------------------------------------------------------------------------- /src/js/tabber.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview 19 | * tabber.js - Provides the main Tabber logic for managing Chrome tabs on 20 | * behalf of a Google user. 21 | * 22 | * Notes: 23 | * - This script is intended to run in the background page. 24 | * - Herein the term "browser session" does not refer to a Chrome "sessions", 25 | * which is just a single tab or single-tabbed Window. Instead, it refers to 26 | * the set of tabs which comprise the state of a browser instance. 27 | * 28 | * Overview 29 | * ======== 30 | * Tabber helps keep the local browser session in sync with a remote session 31 | * saved online. A session has a list of open tabs, a changestamp, and a 32 | * human-friendly description, which will (eventually) allow the user to 33 | * select/navigate among multiple sessions. 34 | * 35 | * Basic UI 36 | * ======== 37 | * Tabber has a status icon (Chrome browse action icon) and a popup page. 38 | * 39 | * The popup page allows the user to perform a few basic Tabber tasks: 40 | * - Set the operational mode (explained below) 41 | * - Perform a manual Save or Restore 42 | * 43 | * The popup also shows a summary of the saved session, including number of 44 | * tabs, and whether it is newer or older than the local one. 45 | * 46 | * The status icon shows status as: 47 | * RED: there is some problem communicating with Chrome or Google 48 | * YELLOW: A sync is needed or is pending. 49 | * GREEN: Current session is properly sync'd with remote 50 | * 51 | * Automatic vs. Manual Operation 52 | * ============================== 53 | * In order to make sync useful yet non-invasive, there are several modes 54 | * of operation that control when/if Tabber automatically tries to sync the 55 | * browser and session states. 56 | * 57 | * MANUAL: No session sync is performed unless user requests it from the UI. 58 | * AUTOSTART: Browser is sync'd to saved session only at Tabber/Browser start. 59 | * AUTOSAVE: Like AUTOSTART, but browser changes are automatically saved. 60 | * AUTOSYNC: Like AUTOSAVE, but the browser is automatically sync'd to any 61 | * remote changes made to the saved session (from another machine). 62 | * 63 | * Internal Operation 64 | * ================== 65 | * Tabber saves or restores the Chrome tabs using the Chrome.storage.sync API, 66 | * which stores extension data in the user's Google account. We don't try to 67 | * sync every move the user makes (i.e. scrolling, zooming, etc.), but we do 68 | * keep track of each tab's url, index, title, and active status. 69 | * When comparing the browser to a saved session, there are three possible 70 | * levels of severity for differences, with Tabber only reporting the most 71 | * severe difference: 72 | * 73 | * Major: A major difference is indicated to the user, and provokes an 74 | * auto-sync operation. An example of a major difference would be 75 | * mismatched tab urls. 76 | * Minor: A minor" difference is not indicated to the user, but still provokes 77 | * an auto-sync. These are differences that may be visible, but not 78 | * substantial. An example of a minor difference would be a mismatch 79 | * of active tabs. 80 | * 81 | * Auto-save operations are done in a time-delayed fashion to avoid 82 | * rapid/repeated online updates. Tabber will wait for at least a few seconds 83 | * of inactivity before initiating any compare or sync operations. 84 | * 85 | */ 86 | 87 | goog.require('TabberInternal.TabberSession'); 88 | 89 | /** 90 | * Global debug output control 91 | * @type {boolean} 92 | */ 93 | var debug = false; 94 | 95 | /** 96 | * Convert a set of tabs into human-readable description. 97 | * @param {Array} tabs - Set of tabs to convert. 98 | * @return {string} 99 | */ 100 | function tabsToString(tabs) { 101 | var output = ''; 102 | var activeTag = ''; 103 | if (tabs.length > 0) { 104 | for (var t = 0; t < tabs.length; t++) { 105 | if (tabs[t].active) { 106 | activeTag = '>('; 107 | } else { 108 | activeTag = ' ('; 109 | } 110 | activeTag += tabs[t].windowId + ')'; 111 | output += activeTag + '\t' + '(' + tabs[t].id + ')' + tabs[t].title + 112 | '\n'; 113 | // output += activeTag + 'TAB: \t' + JSON.stringify(tabs[t]) + '\n'; 114 | } 115 | } else { 116 | output += 'Error: NO TABS found in session!\n'; 117 | } 118 | return output; 119 | } 120 | 121 | 122 | /** 123 | * Define the Tabber object which holds all the Tabber properties. All Tabber 124 | * operations and the API are accessed from a singleton Tabber object which is 125 | * created, initialized and attached to the current (background) page on load. 126 | * @private 127 | * @constructor 128 | * @implements {TabberApi} 129 | */ 130 | function TabberClass() { 131 | // Exported stuff first 132 | this.state = { 133 | ERR: -1, 134 | OK: 0, 135 | WARN: 1, 136 | }; 137 | 138 | // operational modes 139 | this.mode = { 140 | MANUAL: 'manual', 141 | AUTOSTART: 'autostart', 142 | AUTOSAVE: 'autosave', 143 | AUTOSYNC: 'autosync' 144 | }; 145 | 146 | // Tabber-specific constants. 147 | this.constant_ = { 148 | sync_delay: 4000, // delay from last local change to push 149 | urgent_sync_delay: 500, // short delay for quick updates 150 | ok_status: 'Tabs saved', 151 | def_title: 'Click here to manage browser tabs', 152 | // chrome storage keys 153 | options_key: 'options' 154 | }; 155 | // Tabber state vars. 156 | this.uninitialized_ = true; 157 | this.initialized_ = false; 158 | this.remoteSession_ = new TabberInternal.TabberSession(); // init when fetched 159 | this.localSession_ = new TabberInternal.TabberSession(); // locally known tabs 160 | this.change_count_ = 0; // TODO: REMOVE 161 | this.pending_sync_ = false; // scheduled session check 162 | this.pending_update_ = false; // schedule local session update 163 | this.sync_in_progess_ = false; // true while syncing browser to session 164 | this.save_in_progess_ = false; // true while saving browser to session 165 | /** @private */ 166 | this.options_ = {'mode': this.mode.AUTOSAVE}; // default operation 167 | this.windowIds_ = []; // lookup a local windowId by the remote windowId 168 | 169 | // Current status 170 | this.status_ = this.state.OK; 171 | this.statusMessage_ = ''; 172 | } 173 | 174 | /** 175 | * Used once to initialize our state. 176 | * @private 177 | */ 178 | TabberClass.prototype.startInitialization_ = function() { 179 | // If there is no singleton, ignore this call 180 | if (!tabberSingleton) { 181 | consoleErrorLog('Unsolicited tabber initialization!'); 182 | return; 183 | } 184 | // If we already started initialize, ignore this call (shouldn't happen) 185 | if (tabberSingleton && !tabberSingleton.uninitialized_) { 186 | consoleErrorLog('Duplicate tabber initialization!'); 187 | return; 188 | } 189 | var tbr = tabberSingleton; 190 | 191 | // Close the init gate. 192 | tbr.uninitialized_ = false; 193 | 194 | // Fetch our saved config (if any) and continue init. 195 | chrome.storage.local.get('options', tbr.finishInit_); 196 | }; 197 | 198 | /* 199 | * The "API" used by the popup UI consists of the following functions: 200 | * saveLocalToRemote() - manual save local session as remote 201 | * syncBrowserFromRemote() - manual apply remote session to browser 202 | * setOptions() - sets Tabber parameters, which are: 203 | * mode (string) - operational mode ("manual", "autosync", etc.) 204 | * debug (boolean) - enable console logging 205 | * getStatus() - returns an object with the current state 206 | */ 207 | 208 | /** 209 | * Save the local session to the remote account. 210 | * Note that this may be called by the user in Manual mode from the popup UI, 211 | * as well as being called as a result of a session sync. 212 | */ 213 | TabberClass.prototype.saveLocalToRemote = function() { 214 | var tbr = tabberSingleton; 215 | consoleDebugLog('Got a Tabber.saveLocalToRemote call'); 216 | // Don't allow overlapping saves 217 | if (tbr.save_in_progess_) { 218 | consoleTaggedLog('Overlapping Tabber.saveLocalToRemote call ignored'); 219 | return; 220 | } 221 | tbr.save_in_progess_ = true; 222 | /* 223 | * We have to insure our local tabs are in sync with browser. 224 | * to do this, we need a multi-phase approach where the first phase does 225 | * a local session update. 226 | */ 227 | tbr.syncLocalToBrowser_(tbr.saveLocalToRemote_); 228 | }; 229 | 230 | /** 231 | * Sync the local browser state from the currently known remote session. 232 | * Note that this may be called by the user from the popup UI, as well as being 233 | * called from the session sync. 234 | */ 235 | TabberClass.prototype.syncBrowserFromRemote = function() { 236 | // Handy vars. 237 | var tbr = tabberSingleton; 238 | var rem = tbr.remoteSession_; 239 | // if remote session is not valid, don't do anything 240 | if (rem.generation < 0) { 241 | consoleErrorLog('Cannot sync from remote session yet.'); 242 | return; 243 | } 244 | if (rem.tabs.length < 1) { 245 | consoleErrorLog('Cannot sync from Remote session (0 remote tabs)'); 246 | return; 247 | } 248 | consoleDebugLog( 249 | 'Syncing local browser from gen ' + tbr.localSession_.generation + 250 | ' to ' + tbr.remoteSession_.generation); 251 | /* In order to sync the browser with the remote session, we may have to do 252 | * up to four distinct ordered phases of tab operations: 253 | * - create/move new tab(s) to align the session data 254 | * - delete deprecated tabs 255 | * - move tabs to proper windows in Chrome 256 | * - set the new active tabs 257 | * To insure these steps are done in the above order, we count the changes 258 | * needed for each phase and only move on to the next phase when the last 259 | * change is complete 260 | */ 261 | // Tell Tabber to ignore local changes while we are syncing. 262 | tbr.sync_in_progess_ = true; 263 | /* 264 | * We have to insure our local tabs are in sync with browser. 265 | * to do this, we first do a local session update. 266 | */ 267 | tbr.syncLocalToBrowser_(tbr.syncBrowserCreatesAndMoves_); 268 | }; 269 | 270 | /** 271 | * Set/modify Tabber operational parameters. 272 | * @param {TabberApi.type.config} config - The caller's specified option values. 273 | */ 274 | TabberClass.prototype.setOptions = function(config) { 275 | consoleDebugLog('setOptions() call with: ' + JSON.stringify(config)); 276 | if (config.hasOwnProperty('mode')) { 277 | // only accept recognized modes 278 | if ((config.mode === tabberSingleton.mode.AUTOSYNC) || 279 | (config.mode === tabberSingleton.mode.AUTOSTART) || 280 | (config.mode === tabberSingleton.mode.AUTOSAVE) || 281 | (config.mode === tabberSingleton.mode.MANUAL)) { 282 | // TODO: if new mode is AUTO* and sessions are not in sync, then 283 | // alert the user that he must do a manual save/restore before selecting 284 | // the specific AUTO mode. Otherwise, proceed below. 285 | tabberSingleton.options_.mode = config.mode; 286 | consoleDebugLog('New Tabber mode: ' + config.mode); 287 | // Save updated config in Chrome local storage. This will trigger our 288 | // change callback which in turn will run sync. 289 | chrome.storage.local.set({'options': tabberSingleton.options_}); 290 | } else { 291 | alert('Unsupported Tabber operational mode: ' + config.mode); 292 | } 293 | } 294 | if (config.hasOwnProperty('debug')) { 295 | if (config.debug) { 296 | debug = true; 297 | consoleDebugLog('Turning debug ON'); 298 | } else { 299 | consoleDebugLog('Turning debug OFF'); 300 | debug = false; 301 | } 302 | } 303 | }; 304 | 305 | /** 306 | * Get the current Tabber status. 307 | * @return {TabberApi.type.status} 308 | */ 309 | TabberClass.prototype.getStatus = function() { 310 | consoleDebugLog('Copy options: ' + JSON.stringify(tabberSingleton.options_)); 311 | /** type {TabberApi.type.config} */ 312 | var opts = copyObject_(tabberSingleton.options_); 313 | // Add the debug state 314 | opts.debug = debug; 315 | /** 316 | * @type {TabberApi.type.status} 317 | */ 318 | var status = { 319 | options: opts, 320 | sync: {'state': tabberSingleton.status_, 321 | 'msg': tabberSingleton.statusMessage_}, 322 | remote_time: tabberSingleton.remoteSession_.getTimeString() 323 | }; 324 | return status; 325 | }; 326 | 327 | /* 328 | * Internal Tabber code 329 | */ 330 | 331 | /** 332 | * Perform the initial phase of saving a local session, which is just to make 333 | * sure we have the latest tabs from chrome. 334 | * @private 335 | * @param {function()} contFunc - The function to call after tabs are updated. 336 | */ 337 | TabberClass.prototype.syncLocalToBrowser_ = function(contFunc) { 338 | var tbr = tabberSingleton; 339 | consoleDebugLog('Performing syncLocalToBrowser_'); 340 | 341 | // Callback gets the current tabs info 342 | function currentTabs(tabs) { 343 | var loc = tbr.localSession_; 344 | consoleDebugLog('Latest local tabs:\n' + tabsToString(tabs)); 345 | // Accept the new set of tabs 346 | loc.tabs = tabs; 347 | // Proceed with updated tabs in place. 348 | contFunc(); 349 | } 350 | // Get the current tabs from Chrome 351 | chrome.tabs.query({}, currentTabs); 352 | }; 353 | 354 | /** 355 | * Perform the actual save of the local session to the remote account. 356 | * When this phase completes, the real save operation is done. 357 | * @private 358 | */ 359 | TabberClass.prototype.saveLocalToRemote_ = function() { 360 | consoleDebugLog('Performing saveLocalToRemote action'); 361 | var tbr = tabberSingleton; 362 | var loc = tbr.localSession_; 363 | var rem = tbr.remoteSession_; 364 | // Before we flush our local session to remote, make sure we pick a new 365 | // generation number that is highest. 366 | if (loc.generation <= rem.generation) { 367 | loc.generation = rem.generation + 1; 368 | } 369 | // Get a sync object which splits the tab objects into separate elements 370 | // so chrome.staorage.sync can handle it. 371 | var syncObj = loc.toSync(); 372 | 373 | // Now we can update the remote. 374 | consoleDebugLog('Saving current session to remote storage'); 375 | consoleDebugLog('Saving: ' + JSON.stringify(syncObj)); 376 | chrome.storage.sync.set(syncObj, function() { 377 | if (chrome.runtime.lastError) { 378 | // Report error. 379 | tbr.setStatus_(tbr.state.ERR, 'Unable to save session: ' + 380 | chrome.runtime.lastError.message); 381 | } else { 382 | // success updating remote session 383 | tbr.remoteSession_ = new TabberInternal.TabberSession(tbr.localSession_); 384 | // tbr.setStatus_(tbr.state.OK, 'Checking session'); 385 | } 386 | // Note that saving to chrome storage will generate a storage change event, 387 | // which in turn will provoke a doSync 388 | tbr.save_in_progess_ = false; 389 | return; 390 | }); 391 | }; 392 | 393 | 394 | /** 395 | * This function performs the tab creates and index moves when syncing the 396 | * local browser to the remote session. Note that this does not actually 397 | * finalize the real window index, but merely the global tab index in the 398 | * session data. After this phase, we will resolve the tab window indexes. 399 | * @private 400 | */ 401 | TabberClass.prototype.syncBrowserCreatesAndMoves_ = function() { 402 | var tbr = tabberSingleton; 403 | var loc = tbr.localSession_; 404 | var rem = tbr.remoteSession_; 405 | 406 | // Start the work of deleting excess tabs 407 | consoleDebugLog('Creating/aligning tabs'); 408 | 409 | // Init SyncPhaseHandler to manage this phase. 410 | TabberInternal.SyncPhaseHandler.initHandler(tbr.syncBrowserDeletes_); 411 | 412 | /** 413 | * This doStep callback updates our local tab object and restarts the remote 414 | * tab scanning after a chrome tab change is complete. 415 | * @param {*} ctx - Our local tab index for the tab being updated. 416 | * @param {Array} tabArray - Updated chrome tab objects. 417 | */ 418 | function onTabUpdate(ctx, tabArray) { 419 | consoleDebugLog('Chrome updated tab ' + ctx); 420 | // consoleDebugLog("Current tab is:"+JSON.stringify(loc.tabs[ctx])); 421 | // Update our local tab object. 422 | loc.tabs[ctx].index = tabArray[0].index; 423 | loc.tabs[ctx].id = tabArray[0].id; 424 | loc.tabs[ctx].windowId = tabArray[0].windowId; 425 | consoleDebugLog('updated loc.tab to: ' + JSON.stringify(loc.tabs[ctx])); 426 | // Continue scanning. 427 | scanRemoteTabs(); 428 | } 429 | 430 | var remTabIndex = -1; 431 | /** 432 | * Reentrant function that scans the remote tabs and tries to align the local 433 | * tabs to match as best as possible. It will try to move a local tab to a 434 | * matching position in the tabs array if the content matches, or if no match 435 | * is found locally, it will create a new tab to match. 436 | */ 437 | function scanRemoteTabs() { 438 | for (remTabIndex++; remTabIndex < rem.tabs.length; remTabIndex++) { 439 | // consoleDebugLog('Checking tab ' + remTabIndex); 440 | // If corresponding tabs have major difference. 441 | if ((remTabIndex >= loc.tabs.length) || 442 | getTabsDiff(loc.tabs[remTabIndex], 443 | rem.tabs[remTabIndex]).major) { 444 | consoleDebugLog('Tabs at index ' + remTabIndex + ' DO NOT match'); 445 | // check remaining local tabs (if any) for a match 446 | var matched = false; 447 | for (var tt = remTabIndex + 1; tt < loc.tabs.length; tt++) { 448 | // If tab with equivalent content is found 449 | if (!getTabsDiff(loc.tabs[tt], rem.tabs[remTabIndex]).major) { 450 | consoleDebugLog('Moving tab ' + tt + ' to index ' + remTabIndex); 451 | // Update localSession array by moving the matching tab to proper 452 | // position in the array. We will fix the window and index later. 453 | loc.tabs.splice(remTabIndex, 0, loc.tabs.splice(tt, 1)[0]); 454 | // Continue the scan. 455 | matched = true; 456 | break; 457 | } 458 | } 459 | // If we found a matching tab above. 460 | if (matched) { 461 | // Just continue the scan 462 | continue; 463 | } 464 | // If we got here, we couldn't find a matching tab at a different index. 465 | consoleDebugLog('No match for remote tab ' + remTabIndex); 466 | /** 467 | * Create pseudo-tab object to specify the new local tab. 468 | * @type {TabberInt.type.Tab} 469 | */ 470 | var newTab = {index: remTabIndex, url: rem.tabs[remTabIndex].url}; 471 | loc.tabs.splice(remTabIndex, 0, newTab); 472 | // Set a callback so we can grab real ids. 473 | TabberInternal.SyncPhaseHandler.setDoStepCallback(onTabUpdate, remTabIndex); 474 | // Tell chrome to create new browser tab. 475 | TabberInternal.SyncPhaseHandler.doStep(chrome.tabs.create, newTab); 476 | // Update our local tab object with more info. We have to do this 477 | // after the chrome call, because chrome doesn't like these object keys. 478 | consoleDebugLog('Adding title and active state'); 479 | newTab.title = rem.tabs[remTabIndex].title; 480 | newTab.active = rem.tabs[remTabIndex].active; 481 | consoleDebugLog('loc.tab: ' + JSON.stringify(loc.tabs[remTabIndex])); 482 | // stop here and let step callback continue the scan. 483 | return; 484 | } else { 485 | consoleDebugLog('Tabs at index ' + remTabIndex + ' seem to match'); 486 | } 487 | } 488 | // If we got here, we are done with the scan and this phase. 489 | TabberInternal.SyncPhaseHandler.finalize(); 490 | } 491 | // Kick off the remote scan 492 | scanRemoteTabs(); 493 | }; 494 | 495 | 496 | /** 497 | * This function performs the tab deletes when syncing the local browser. 498 | * @private 499 | */ 500 | TabberClass.prototype.syncBrowserDeletes_ = function() { 501 | var tbr = tabberSingleton; 502 | var loc = tbr.localSession_; 503 | var rem = tbr.remoteSession_; 504 | 505 | // Start the work of deleting excess tabs 506 | consoleDebugLog( 507 | 'Removing ' + (loc.tabs.length - rem.tabs.length) + ' excess local tabs'); 508 | 509 | // Init SyncPhaseHandler to manage this phase. 510 | TabberInternal.SyncPhaseHandler.initHandler(tbr.syncBrowserChromeMapWindows_); 511 | 512 | // Delete any excess local tabs 513 | if (loc.tabs.length > rem.tabs.length) { 514 | for (var t = rem.tabs.length; t < loc.tabs.length; t++) { 515 | consoleDebugLog('Removing tab ' + t); 516 | TabberInternal.SyncPhaseHandler.doStep(chrome.tabs.remove, loc.tabs[t].id); 517 | } 518 | // Adjust local tabs array. 519 | loc.tabs.splice(rem.tabs.length, loc.tabs.length - rem.tabs.length); 520 | } 521 | // Done with this phase. 522 | TabberInternal.SyncPhaseHandler.finalize(); 523 | 524 | }; 525 | 526 | /** 527 | * Compute an affinity score based on how similar the two sets of tabs are. 528 | * @private 529 | * @param {Array} tabs1 - First array of tabs to compare. 530 | * @param {Array} tabs2 - First array of tabs to compare. 531 | * @return {number} - returns the score. 532 | */ 533 | function getWindowScore_(tabs1, tabs2) { 534 | var score = 0; 535 | // we award 5 pts for matching URLs 536 | // we award another 3 pts for matching index 537 | // we award another 1 pt for matching active state 538 | for (var t1 = 0; t1 < tabs1.length; t1++) { 539 | for (var t2 = 0; t2 < tabs2.length; t2++) { 540 | if (tabs1[t1].url == tabs2[t2].url) { 541 | score += 5; 542 | // Now check for matching index 543 | if (tabs1[t1].index == tabs2[t2].index) { 544 | score += 3; 545 | } 546 | // Check for matching active state 547 | if (tabs1[t1].active == tabs2[t2].active) { 548 | score += 1; 549 | } 550 | } 551 | } 552 | } 553 | return score; 554 | } 555 | 556 | /** 557 | * This function maps remote windows to local windows. 558 | * @private 559 | */ 560 | TabberClass.prototype.syncBrowserChromeMapWindows_ = function() { 561 | var tbr = tabberSingleton; 562 | var loc = tbr.localSession_; 563 | var rem = tbr.remoteSession_; 564 | 565 | // Start the work of mapping windows 566 | consoleDebugLog('Mapping remote and local windows'); 567 | consoleDebugLog('Current Remote windows:\n' + tabsToString(rem.tabs)); 568 | consoleDebugLog('Current Local windows:\n' + tabsToString(loc.tabs)); 569 | 570 | /* The main computational goal of this phase is to pick the best mapping 571 | * between remote windows and local windows. If there are more remote windows 572 | * than existing local ones, this phase will also create new local windows 573 | * to match. At the end of this phase, there will be a local window for each 574 | * remote window, and the local session data will have the intended window 575 | * Ids for each tab. The next phase will insure that all tabs reside in the 576 | * correct window with the correct index. 577 | */ 578 | 579 | // Build lists of existing remote and local windowIds 580 | var remWindows = []; 581 | var locWindows = []; 582 | for (var t = 0; t < rem.tabs.length; t++) { 583 | // if this is a new windowId 584 | // add this window (if not found) and tab 585 | if (!(rem.tabs[t].windowId in remWindows)) { 586 | // add tab to new windowId record 587 | remWindows[rem.tabs[t].windowId] = [rem.tabs[t]]; 588 | } else { 589 | remWindows[rem.tabs[t].windowId].push(rem.tabs[t]); 590 | } 591 | } 592 | for (var t = 0; t < loc.tabs.length; t++) { 593 | // @type number 594 | var windowId = loc.tabs[t].windowId; 595 | // add this window (if not found) and tab 596 | if (!(windowId in locWindows)) { 597 | locWindows[windowId] = [loc.tabs[t]]; 598 | } else { 599 | locWindows[windowId].push(loc.tabs[t]); 600 | } 601 | } 602 | // Score each possible map edge. 603 | var mapScores = []; 604 | for (var lwid in locWindows) { 605 | for (var rwid in remWindows) { 606 | var score = getWindowScore_(locWindows[Number(lwid)], 607 | remWindows[Number(rwid)]); 608 | mapScores.push({'lwid': lwid, 609 | 'rwid': rwid, 610 | 'score': score 611 | }); 612 | } 613 | } 614 | // Sort the maps by score - highest to lowest. 615 | var sortedMapScores = mapScores.sort(function(a, b) { 616 | if (a.score == b.score) return 0; 617 | return (a.score < b.score) ? 1 : -1; 618 | }); 619 | 620 | /* 621 | * Now we have a sorted list of scores. We need to walk the list and accept 622 | * the highest scoring match that involves windowIds that have not already 623 | * been matched. 624 | */ 625 | var matchedRemIds = []; 626 | var matchedLocIds = []; 627 | var locIdByRemId = []; // final map 628 | 629 | // Take the highest score as a valid mapping. 630 | for (var mi = 0; mi < sortedMapScores.length; mi++) { 631 | // If we haven't mapped these ids yet. 632 | if ((matchedRemIds.indexOf(sortedMapScores[mi]['rwid']) == -1) && 633 | (matchedLocIds.indexOf(sortedMapScores[mi]['lwid']) == -1)) { 634 | // Take this mapping. 635 | consoleDebugLog( 636 | 'Mapping rwid ' + sortedMapScores[mi]['rwid'] + ' to lwid ' + 637 | sortedMapScores[mi]['lwid']); 638 | locIdByRemId[sortedMapScores[mi]['rwid']] = sortedMapScores[mi]['lwid']; 639 | // And don't reconsider either local or remote ids. 640 | matchedRemIds.push(sortedMapScores[mi]['rwid']); 641 | matchedLocIds.push(sortedMapScores[mi]['lwid']); 642 | } 643 | } 644 | 645 | /* 646 | * Now we have our mapping, so for each tab, we want to move it to the 647 | * corresponding local window. We may have to create new windows as we go 648 | */ 649 | 650 | /** 651 | * This function is called after a new window and initial tab are created, 652 | * to add the new window Id to the window map, then continue iterating. 653 | * @param {(ChromeWindow|null)} newWindow - The new window Object from chrome. 654 | */ 655 | function acceptNewWindow(newWindow) { 656 | // Get index of tab just moved to new window 657 | var ti = tbr.context_ - 1; 658 | consoleDebugLog( 659 | 'Tab ' + loc.tabs[ti].id + ' assigned to new window ' + newWindow.id); 660 | consoleDebugLog( 661 | 'New rem->loc map: ' + rem.tabs[ti].windowId + ' -> ' + newWindow.id); 662 | // Update the local window Id that maps to the remote window. 663 | locIdByRemId[rem.tabs[ti].windowId] = newWindow.id; 664 | // And the local session data for the rehomed tab 665 | loc.tabs[ti].windowId = newWindow.id; 666 | // now continue 667 | continueRehomingTabs(); 668 | } 669 | 670 | /* this is the re-entrant function that works its way through the tabs, 671 | * creating windows as needed, or moving tabs to correct window 672 | */ 673 | function continueRehomingTabs() { 674 | // Make each tab get displayed in the correct window. 675 | for (var t = tbr.context_; t < loc.tabs.length; t++) { 676 | var lwid = locIdByRemId[rem.tabs[t].windowId]; 677 | if (typeof lwid == 'undefined') { 678 | // set re-entrant position 679 | tbr.context_ = t + 1; 680 | consoleDebugLog('No lwid found for rwid ' + rem.tabs[t].windowId); 681 | // create new window, then continue 682 | chrome.windows.create({tabId: loc.tabs[t].id}, acceptNewWindow); 683 | // stop here and let async continuation resume the loop 684 | return; 685 | } else if (loc.tabs[t].windowId != lwid) { 686 | // Move this tab to correct window (indexes are adjusted in next phase) 687 | consoleDebugLog( 688 | 'Moving tab ' + loc.tabs[t].id + ' from window ' + 689 | loc.tabs[t].windowId + ' to ' + lwid); 690 | // Update local session data. 691 | loc.tabs[t].windowId = lwid; 692 | // Tell chrome to do the move. 693 | chrome.tabs.move(loc.tabs[t].id, {windowId: Number(lwid), index: 0}); 694 | } 695 | } 696 | // Done with this phase. 697 | tbr.syncBrowserChromeMove_(); 698 | } 699 | // Just set the initial tab index context and start rehoming tabs 700 | tbr.context_ = 0; 701 | continueRehomingTabs(); 702 | }; 703 | 704 | /** 705 | * This function moves tabs to proper window index in Chrome. 706 | * @private 707 | */ 708 | TabberClass.prototype.syncBrowserChromeMove_ = function() { 709 | var tbr = tabberSingleton; 710 | var loc = tbr.localSession_; 711 | var rem = tbr.remoteSession_; 712 | 713 | // Start the work of moving tabs 714 | // consoleDebugLog('Correcting tab indexes'); 715 | // consoleDebugLog("Remote windows:\n" + tabsToString(rem.tabs)); 716 | // consoleDebugLog("Local windows:\n" + tabsToString(loc.tabs)); 717 | 718 | // Init SyncPhaseHandler to manage this phase. 719 | TabberInternal.SyncPhaseHandler.initHandler(tbr.syncBrowserSetActive_); 720 | 721 | // Make each tab get displayed in the correct window with the correct index. 722 | for (var t = 0; t < loc.tabs.length; t++) { 723 | // consoleDebugLog("Moving tab "+loc.tabs[t].id+" to index 724 | // "+rem.tabs[t].index); 725 | TabberInternal.SyncPhaseHandler.doStep(chrome.tabs.move, 726 | loc.tabs[t].id, {index: rem.tabs[t].index}); 727 | } 728 | // Done with this phase. 729 | TabberInternal.SyncPhaseHandler.finalize(); 730 | }; 731 | 732 | /** 733 | * This function sets the active tab when syncing the local browser 734 | * @private 735 | */ 736 | TabberClass.prototype.syncBrowserSetActive_ = function() { 737 | var tbr = tabberSingleton; 738 | var loc = tbr.localSession_; 739 | var rem = tbr.remoteSession_; 740 | 741 | // Define what to do when all the steps of this phase are done. 742 | function phaseDone() { 743 | // Tell Tabber to respond to local changes again. 744 | consoleDebugLog('Re-enabling change monitor'); 745 | tbr.sync_in_progess_ = false; 746 | // Schedule a local sesion update in case some local changes happened while 747 | // we were syncing and ignoring local changes. 748 | tbr.scheduleLocalSessionUpdate_(tbr.constant_.sync_delay); 749 | } 750 | 751 | // Start the work of setting active states. 752 | consoleDebugLog('Finalizing local session...'); 753 | 754 | // Init SyncPhaseHandler to manage this phase. 755 | TabberInternal.SyncPhaseHandler.initHandler(phaseDone); 756 | 757 | // Walk the remote tabs and set the active states. 758 | for (var t = 0; t < loc.tabs.length; t++) { 759 | // Set the local session data active state 760 | loc.tabs[t].active = rem.tabs[t].active; 761 | // And add a step to tell Chrome to do it too. 762 | TabberInternal.SyncPhaseHandler.doStep(chrome.tabs.update, 763 | loc.tabs[t].id, {'active': rem.tabs[t].active}); 764 | } 765 | // Complete this phase 766 | TabberInternal.SyncPhaseHandler.finalize(); 767 | }; 768 | 769 | /** 770 | * Reschedule a local session update. This is called after any local browser 771 | * changes to (re)schedule a local session eval/update. 772 | * @private 773 | * @param {number} delay - Number of milliseconds to delay before update. 774 | * @return {boolean} - True if update was done or scheduled OK. 775 | */ 776 | TabberClass.prototype.scheduleLocalSessionUpdate_ = function(delay) { 777 | var tbr = tabberSingleton; 778 | // Cancel any pending update since we are rescheduling. 779 | if (tbr.pending_update_) { 780 | clearTimeout(tbr.pending_update_); 781 | } 782 | // If we are syncing, we ignore this event (leave unscheduled). 783 | if (tbr.sync_in_progess_) { 784 | return false; 785 | } 786 | tbr.setStatus_(tbr.state.WARN, 'Local change detected - update pending'); 787 | var scheduleDelay_ms = delay; 788 | // if we are starting a new session, insure a short delay 789 | if (tbr.localSession_.generation < 2) { 790 | if (scheduleDelay_ms > tbr.constant_.urgent_sync_delay) { 791 | scheduleDelay_ms = tbr.constant_.urgent_sync_delay; 792 | } 793 | } 794 | // Schedule the update. 795 | consoleDebugLog( 796 | 'Scheduling update from browser event, with delay = ' + scheduleDelay_ms); 797 | tbr.pending_update_ = 798 | setTimeout(tbr.updateLocalSessionFromBrowser_, scheduleDelay_ms); 799 | return true; 800 | }; 801 | 802 | /** 803 | * This function is called after a change to the local or remote session. The 804 | * primary indication of which is 'newer' is the argument to this function. 805 | * If it is not known, the session generation number is used as a tiebreaker 806 | * to reconcile differences. This function then determines what actions to take 807 | * in response to the change. It sets the status indications, and can kick off 808 | * a session save or local browser sync, as needed. 809 | * @private 810 | * @param {boolean=} localIsNewer - Indicates whether local or remote side just 811 | * changed. If not specified, we make a guess. 812 | */ 813 | TabberClass.prototype.doSync_ = function(localIsNewer) { 814 | var tbr = tabberSingleton; 815 | var rem = tbr.remoteSession_; 816 | var loc = tbr.localSession_; 817 | // We need to know if this is the first sync of a new session. if so, we 818 | // generally do one sync before starting any auto-save operations. 819 | var firstTime = (loc.generation == 1); 820 | // If caller doesn't tell us what changed, just guess based on generation. 821 | if (typeof localIsNewer == 'undefined') { 822 | consoleDebugLog('Guessing change'); 823 | // if in AUTOSAVE mode and this is not firstTime sync, then always assume 824 | // local session is newer. 825 | var forceNewLocal = 826 | ((tbr.options_.mode == tbr.mode.AUTOSAVE) && !firstTime); 827 | // now set authoritative session 828 | localIsNewer = forceNewLocal || (loc.generation >= rem.generation); 829 | } 830 | if (localIsNewer) { 831 | consoleDebugLog('Syncing local update...'); 832 | } else { 833 | consoleDebugLog('Syncing remote update...'); 834 | } 835 | // compare local and remote sessions 836 | var diff = getSessionDiff(loc, rem); 837 | consoleDebugLog('Got dif report: ' + JSON.stringify(diff)); 838 | // Set status from diff report 839 | if (diff.major) { 840 | // If there is a major dif, status will always be ERROR or WARN 841 | if (diff.err) { 842 | tbr.setStatus_(tbr.state.ERR, diff.major); 843 | } else { 844 | tbr.setStatus_(tbr.state.WARN, diff.major); 845 | } 846 | } else if (diff.minor) { 847 | // For minor differences, we keep status OK, but set the minor msg. 848 | tbr.setStatus_(tbr.state.OK, diff.minor); 849 | } else { 850 | // No differences found. 851 | tbr.setStatus_(tbr.state.OK, tbr.constant_.ok_status); 852 | } 853 | // Now determine whether to do any automatic sync operation. 854 | // If we do not have both sessions initialized, we can't do anything yet. 855 | if ((rem.generation < 0) || (loc.generation < 0)) { 856 | consoleDebugLog('Deferring sync until session initialization'); 857 | return; 858 | } 859 | // If we don't have a valid local session, we have to keep waiting 860 | if (!isSessionValid(loc)) { 861 | consoleErrorLog('Deferring sync until local session established'); 862 | return; 863 | } 864 | // If we have found a difference. 865 | if (diff.major || diff.minor) { 866 | consoleDebugLog('Found differences with favor_local = ' + localIsNewer); 867 | consoleDebugLog('loc.gen=' + loc.generation); 868 | consoleDebugLog('rem.gen=' + rem.generation); 869 | // For newer local session 870 | if (localIsNewer) { 871 | // If this is a first time change, we don't auto save. 872 | if (!firstTime) { 873 | // In autosync and autosave mode, save the updated local session. 874 | // For all other modes, we never save automatically. 875 | if ((tbr.options_.mode == tbr.mode.AUTOSYNC) || 876 | (tbr.options_.mode == tbr.mode.AUTOSAVE)) { 877 | tbr.saveLocalToRemote(); 878 | } 879 | } 880 | } else { // for newer remote session 881 | /* If we do not have a valid remote session, it means we couldn't 882 | * establish one, so just adopt the valid local one 883 | */ 884 | if (!isSessionValid(rem)) { 885 | rem = loc; 886 | consoleDebugLog('No remote session found'); 887 | // Recursively call back to insure we are all sync'd. Pretend it's as a 888 | // result of a local change, so it will only sync the remote bits. 889 | tbr.doSync_(true); 890 | return; 891 | } 892 | // do a local browser sync for the following modes: 893 | // autosync, autostart(first time only), and autosave(first time only) 894 | if ((tbr.options_.mode == tbr.mode.AUTOSYNC) || 895 | ((tbr.options_.mode == tbr.mode.AUTOSTART) && firstTime) || 896 | ((tbr.options_.mode == tbr.mode.AUTOSAVE) && firstTime)) { 897 | tbr.syncBrowserFromRemote(); 898 | } 899 | } 900 | } 901 | }; 902 | 903 | /** 904 | * Ask Chrome for the current set of tabs, and build/update our local session 905 | * with the resulting tab information. 906 | * This is called at the end of Tabber initialization, and also after any local 907 | * browser change. 908 | * 909 | * @private 910 | */ 911 | TabberClass.prototype.updateLocalSessionFromBrowser_ = function() { 912 | var tbr = tabberSingleton; 913 | consoleDebugLog( 914 | 'updateLocalSessionFromBrowser_() called - getting local tabs'); 915 | // Callback gets the current tabs info 916 | function currentTabs(tabs) { 917 | var loc = tbr.localSession_; 918 | consoleDebugLog('HAD local tabs:\n' + tabsToString(loc.tabs)); 919 | consoleDebugLog('GOT local tabs:\n' + tabsToString(tabs)); 920 | // Accept the new set of tabs 921 | loc.tabs = tabs; 922 | // if this is local session initialization, make generation = 0 923 | if (loc.generation < 0) { 924 | loc.generation = 0; 925 | } 926 | // Touch the session to update timestamp and generation. 927 | loc.touch(); 928 | // For first-time init, we go init remote session now. 929 | if (!tbr.initialized_) { 930 | // Continue initialization by fetching remote storage. 931 | chrome.storage.sync.get(null, tbr.onStorageGet); 932 | } else { // after normal update go sync 933 | // Update status and session states. Note that this path can happen during 934 | // post-init syncing 935 | tbr.doSync_(); 936 | } 937 | } 938 | // Get the current tabs from Chrome 939 | chrome.tabs.query({}, currentTabs); 940 | }; 941 | 942 | 943 | /** 944 | * This is the event handler which is called on any tab remove 945 | * @private 946 | * @param {string} tabId - Id of tab being removed. 947 | * @param {Object} remInfo - hold info about the removal event. 948 | */ 949 | TabberClass.prototype.onTabRemove_ = function(tabId, remInfo) { 950 | var tbr = tabberSingleton; 951 | var delay = tbr.constant_.sync_delay; 952 | // If the window is closing, don't delay the update. 953 | if (remInfo['isWindowClosing']) { 954 | delay = tbr.constant_.urgent_sync_delay; 955 | } 956 | // schedule a session update 957 | tbr.scheduleLocalSessionUpdate_(delay); 958 | }; 959 | 960 | /** 961 | * This is the event handler which is called on any tab change 962 | * @private 963 | */ 964 | TabberClass.prototype.onTabChange_ = function() { 965 | var tbr = tabberSingleton; 966 | var delay = tbr.constant_.sync_delay; 967 | // schedule a session update 968 | tbr.scheduleLocalSessionUpdate_(delay); 969 | }; 970 | 971 | /** 972 | * Enables or disables the change event handlers. 973 | * @private 974 | * @param {boolean} enable - New enable setting. 975 | */ 976 | TabberClass.prototype.enableHandlers_ = function(enable) { 977 | var tbr = tabberSingleton; 978 | if (enable) { 979 | consoleDebugLog('Enabling tab event handlers'); 980 | chrome.tabs.onCreated.addListener(tbr.onTabChange_); 981 | chrome.tabs.onUpdated.addListener(tbr.onTabChange_); 982 | chrome.tabs.onMoved.addListener(tbr.onTabChange_); 983 | chrome.tabs.onActivated.addListener(tbr.onTabChange_); 984 | chrome.tabs.onHighlighted.addListener(tbr.onTabChange_); 985 | chrome.tabs.onDetached.addListener(tbr.onTabChange_); 986 | chrome.tabs.onAttached.addListener(tbr.onTabChange_); 987 | chrome.tabs.onRemoved.addListener(tbr.onTabRemove_); 988 | chrome.tabs.onReplaced.addListener(tbr.onTabChange_); 989 | // chrome.tabs.onZoomChange.addListener(tbr.onTabChange_); 990 | } else { 991 | consoleDebugLog('Removing tab event handlers'); 992 | chrome.tabs.onCreated.removeListener(tbr.onTabChange_); 993 | chrome.tabs.onUpdated.removeListener(tbr.onTabChange_); 994 | chrome.tabs.onMoved.removeListener(tbr.onTabChange_); 995 | chrome.tabs.onActivated.removeListener(tbr.onTabChange_); 996 | chrome.tabs.onHighlighted.removeListener(tbr.onTabChange_); 997 | chrome.tabs.onDetached.removeListener(tbr.onTabChange_); 998 | chrome.tabs.onAttached.removeListener(tbr.onTabChange_); 999 | chrome.tabs.onRemoved.removeListener(tbr.onTabRemove_); 1000 | chrome.tabs.onReplaced.removeListener(tbr.onTabChange_); 1001 | // chrome.tabs.onZoomChange.removeListener(tbr.onTabChange_); 1002 | } 1003 | }; 1004 | 1005 | /** 1006 | * Second phase of initialization. 1007 | * @private 1008 | * @param {Object} items - storage items fetched. 1009 | */ 1010 | TabberClass.prototype.finishInit_ = function(items) { 1011 | var tbr = tabberSingleton; 1012 | consoleDebugLog('Current Options: ' + JSON.stringify(tbr.options_)); 1013 | if (items.options) { 1014 | consoleDebugLog('New Options: ' + JSON.stringify(items.options)); 1015 | tbr.options_ = items.options; 1016 | } 1017 | // continue init by querying Chrome for current tabs. 1018 | tbr.updateLocalSessionFromBrowser_(); 1019 | }; 1020 | 1021 | /** 1022 | * Callback from chrome which provides an object with all saved properties. 1023 | * Used to initialize the remote session during Tabber intialization. 1024 | * 1025 | * @param {!Object} obj - Storage object fetched from chrome.storage. 1026 | */ 1027 | TabberClass.prototype.onStorageGet = function(obj) { 1028 | var tbr = tabberSingleton; 1029 | consoleDebugLog('Object from chrome storage: ' + JSON.stringify(obj)); 1030 | // start with an empty session 1031 | tbr.remoteSession_ = new TabberInternal.TabberSession(0); 1032 | var remObjs = tbr.remoteSession_.updateProps(obj); 1033 | // Get rid of obsolete data. 1034 | if (remObjs.length > 0) { 1035 | consoleDebugLog('Removing excess data from remote storage: ' + remObjs); 1036 | chrome.storage.sync.remove(remObjs); 1037 | } 1038 | // Resolve status and session states with new remote session. 1039 | tbr.doSync_(); 1040 | // Complete initialization by turning on event handlers 1041 | tbr.initialized_ = true; 1042 | tbr.enableHandlers_(true); 1043 | chrome.storage.onChanged.addListener(tbr.onChromeStorageChange_); 1044 | }; 1045 | 1046 | /** 1047 | * This is the chrome.storage.sync listener callback for remote session changes. 1048 | * @private 1049 | * @type {function(!Object, string)} 1050 | */ 1051 | TabberClass.prototype.onChromeStorageChange_ = function(changes, namespace) { 1052 | var tbr = tabberSingleton; 1053 | var key; 1054 | // updated session properties come from chrome.storage.sync 1055 | consoleDebugLog('Chrome storage change event: ' + JSON.stringify(changes)); 1056 | // {"options":{"newValue":{"mode":"manual"}}} 1057 | // Make a change object using newValues 1058 | var changeObj = {}; 1059 | for (key in changes) { 1060 | changeObj[key] = changes[key].newValue; 1061 | } 1062 | // Update the session. 1063 | tbr.remoteSession_.updateProps(changeObj); 1064 | // Update status and resolve session state with new remote info. 1065 | tbr.doSync_(false); 1066 | 1067 | // look for updated options 1068 | if (tbr.constant_.options_key in changes) { 1069 | consoleDebugLog('Chrome storage TABBER OPTIONS change'); 1070 | tbr.options_ = copyObject_(changes[tbr.constant_.options_key].newValue); 1071 | // Update status and resolve session state based on new mode. 1072 | tbr.doSync_(); 1073 | } 1074 | }; 1075 | 1076 | /** 1077 | * Called to set our displayed icon and status text. 1078 | * @private 1079 | * @param {TabberApi.type.state} status - The new state. 1080 | * @param {string=} opt_errMessage - Description of the current state. 1081 | */ 1082 | TabberClass.prototype.setStatus_ = function(status, opt_errMessage) { 1083 | tabberSingleton.status_ = status; 1084 | if (typeof opt_errMessage == 'undefined') { 1085 | opt_errMessage = 'No status information'; 1086 | } 1087 | tabberSingleton.statusMessage_ = opt_errMessage; 1088 | consoleDebugLog('Setting status: ' + opt_errMessage); 1089 | // set our badge icons (status color) 1090 | switch (status) { 1091 | case tabberSingleton.state.OK: 1092 | consoleDebugLog('Setting status to OK'); 1093 | // set badge icon GREEN 1094 | chrome.browserAction.setIcon({ 1095 | path: { 1096 | 19: 'images/tabber_green_19.png', 1097 | 38: 'images/tabber_green_38.png' 1098 | } 1099 | }); 1100 | // make title default message 1101 | chrome.browserAction.setTitle({ 1102 | title: tabberSingleton.constant_.def_title 1103 | }); 1104 | break; 1105 | case tabberSingleton.state.WARN: 1106 | consoleDebugLog('Setting status to WARN'); 1107 | // set badge icon YELLOW 1108 | chrome.browserAction.setIcon({ 1109 | path: { 1110 | 19: 'images/tabber_yellow_19.png', 1111 | 38: 'images/tabber_yellow_38.png' 1112 | } 1113 | }); 1114 | // make title warning message 1115 | chrome.browserAction.setTitle({title: opt_errMessage}); 1116 | break; 1117 | default: 1118 | consoleDebugLog('Setting status to ERROR'); 1119 | // set badge icon RED 1120 | chrome.browserAction.setIcon({ 1121 | path: { 1122 | 19: 'images/tabber_red_19.png', 1123 | 38: 'images/tabber_red_38.png' 1124 | } 1125 | }); 1126 | // make title error message 1127 | chrome.browserAction.setTitle({title: opt_errMessage}); 1128 | break; 1129 | } 1130 | }; 1131 | 1132 | /** 1133 | * Helper to copy any object. Only handles primitive props. 1134 | * @private 1135 | * @param {Object} obj - The object to copy. 1136 | * @return {Object} 1137 | */ 1138 | function copyObject_(obj) { 1139 | return Object(JSON.parse(JSON.stringify(obj))); 1140 | } 1141 | 1142 | /** 1143 | * Helper for console logging with timestamp. 1144 | * @param {string} msg - The msg to show. 1145 | */ 1146 | function consoleTaggedLog(msg) { 1147 | var d = new Date(); 1148 | console.log('Tabber@' + d.toLocaleTimeString() + ': ' + msg); 1149 | } 1150 | 1151 | /** 1152 | * Produce filtered DEBUG log messages. 1153 | * @param {string} msg - The msg to show. 1154 | */ 1155 | function consoleDebugLog(msg) { 1156 | if (debug) { 1157 | consoleTaggedLog('DEBUG: ' + msg); 1158 | } 1159 | } 1160 | 1161 | /** 1162 | * Unfiltered Tagged ERROR log messages. 1163 | * @param {string} msg - The msg to show. 1164 | */ 1165 | function consoleErrorLog(msg) { 1166 | consoleTaggedLog('ERROR: ' + msg); 1167 | } 1168 | 1169 | /* 1170 | * Here is the Tabber loadtime operation. Auto-attach a Tabber singleton to the 1171 | * hosting page object. This singleton has the properties which provide the 1172 | * public access function API as well as all internal code and state. 1173 | */ 1174 | var tabberSingleton = new TabberClass(); 1175 | window['Tabber'] = tabberSingleton; 1176 | 1177 | // Do the one-time initialization for our singleton. 1178 | tabberSingleton.startInitialization_(); 1179 | 1180 | // Do the exports 1181 | goog.exportProperty(TabberClass.prototype, 'getStatus', 1182 | TabberClass.prototype.getStatus); 1183 | goog.exportProperty(TabberClass.prototype, 'setOptions', 1184 | TabberClass.prototype.setOptions); 1185 | goog.exportProperty(TabberClass.prototype, 'saveLocalToRemote', 1186 | TabberClass.prototype.saveLocalToRemote); 1187 | goog.exportProperty(TabberClass.prototype, 'syncBrowserFromRemote', 1188 | TabberClass.prototype.syncBrowserFromRemote); 1189 | 1190 | consoleDebugLog('tabber.js load complete'); 1191 | -------------------------------------------------------------------------------- /src/js/tabber_session.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview 19 | * tabber_session.js - Define and export the external Session object class 20 | * which represents a set of browser tabs. 21 | */ 22 | goog.provide('TabberInternal.TabberSession'); 23 | 24 | /** 25 | * Define the TabberSession object which holds the tabs and metadata for a 26 | * single session. This is the same for remote or local sessions. The optional 27 | * constructor argument may be an initial generation number or another 28 | * TabberSession object from which to clone the session data. 29 | * @constructor 30 | * @param {Object|number=} opt_initial - The optional initialization data. 31 | * @implements {TabberInt.TabberSession} 32 | */ 33 | TabberInternal.TabberSession = function(opt_initial) { 34 | // Construction type is determined by argument type 35 | // If arg is an object, assume it is another session to clone 36 | // otherwise (arg is number or missing) construct a new default session 37 | if (typeof opt_initial == 'object') { 38 | // To clone a session, copy it's data props. 39 | var clone = JSON.parse(JSON.stringify(opt_initial)); 40 | for (var key in clone) { 41 | this[key] = clone[key]; 42 | } 43 | // Insure the numtabs prop is set correctly. 44 | this.numtabs = this.tabs.length 45 | } else { 46 | // Creating a brand new session. 47 | this.description = 'default'; 48 | // set generation number 49 | if (typeof opt_initial == 'number') { 50 | // initial generation number specified 51 | this.generation = opt_initial; 52 | } else { 53 | // default initial gen 54 | this.generation = -1; 55 | } 56 | // No tabs by default 57 | this.tabs = []; 58 | this.numtabs = 0; 59 | // No timestamp yet 60 | this.updateTime = false; 61 | } 62 | }; 63 | 64 | /** 65 | * Called when there is a change, in order to update timestamps/generation. 66 | */ 67 | TabberInternal.TabberSession.prototype.touch = function() { 68 | var d = new Date(); 69 | this.updateTime = d.getTime() - (60 * 1000 * d.getTimezoneOffset()); 70 | this.generation++; 71 | consoleDebugLog('Session gen is now ' + this.generation); 72 | }; 73 | 74 | /** 75 | * Return the last touch time as a user string. If this session has not yet 76 | * been initialized, it returns an empty string. 77 | * @return {string} 78 | */ 79 | TabberInternal.TabberSession.prototype.getTimeString = function() { 80 | if (this.updateTime) { 81 | // get a Date object for the local timezone 82 | var locDate = new Date(); 83 | // Build a Date object set to the local time of the session timestamp. 84 | var d = new Date(this.updateTime + 85 | (60 * 1000 * locDate.getTimezoneOffset())); 86 | return d.toLocaleString(); 87 | } 88 | return ''; 89 | }; 90 | 91 | /** 92 | * Return a sync object, which provides key/value pairs for the data in the 93 | * object in a form that chrome.storage.sync can handle. 94 | * @return {Object} 95 | */ 96 | TabberInternal.TabberSession.prototype.toSync = function() { 97 | // consoleDebugLog("Sess: "+JSON.stringify(tabSession)); 98 | var syncObj = {}; 99 | for (var property in this) { 100 | // Save our custom properties. 101 | if (this.hasOwnProperty(property)) { 102 | // skip tabs array property 103 | if (property != 'tabs') { 104 | syncObj[property] = this[property]; 105 | } 106 | } 107 | } 108 | // Now add the tab objects 109 | syncObj['numtabs'] = this.tabs.length; 110 | for (var t = 0; t < this.tabs.length; t++) { 111 | syncObj['Tab_' + t] = this.tabs[t]; 112 | } 113 | return syncObj; 114 | }; 115 | 116 | /** 117 | * Given a single property value, update the session. 118 | * @param {string} key - Property name. 119 | * @param {Object} value - Property value. 120 | * @return {boolean} - whether property is recognized and accepted. 121 | */ 122 | TabberInternal.TabberSession.prototype.update = function(key, value) { 123 | if (!isPropertyValid(key)) { 124 | return false; 125 | } 126 | consoleDebugLog('UPDATING session ' + key + ' : ' + value); 127 | if (key.startsWith('Tab_')) { 128 | var t = parseInt(key.split('_')[1], 10); 129 | // ignore tabs beyond numtabs limit 130 | if (t >= this.numtabs) { 131 | consoleDebugLog('Ignoring outdated tab ' + t); 132 | return false; 133 | } 134 | this.tabs[t] = value; 135 | } else { // for non-Tabs, we just take the property value. 136 | this[key] = value; 137 | // If we update the numtabs property, trim tabs array if needed. 138 | if ((key == 'numtabs') && (value < this.tabs.length)) { 139 | consoleDebugLog('Resetting tabs length to ' + value); 140 | this.tabs = this.tabs.slice(0, value); 141 | this.numtabs = value; 142 | } 143 | } 144 | // property updated 145 | return true; 146 | }; 147 | 148 | /** 149 | * Given an object full of updated session properties, update the session. 150 | * @param {Object} props - Property values. 151 | * @returns {Array} - array of unwanted data keys 152 | */ 153 | TabberInternal.TabberSession.prototype.updateProps = function(props) { 154 | var obsoleteKeys = []; 155 | // Always update the 'numtabs' property first 156 | if ('numtabs' in props) { 157 | this.update('numtabs', props['numtabs']); 158 | } else { 159 | consoleDebugLog('Update keeps number of tabs at '+ this.numtabs); 160 | } 161 | // now iterate all props 162 | for (var key in props) { 163 | if (!this.update(key, props[key])) { 164 | obsoleteKeys.push(key); 165 | } 166 | } 167 | return obsoleteKeys; 168 | }; 169 | 170 | /** 171 | * Given a sync object, set the session properties to match. 172 | * @param {Object} syncObj - Sync object of a session. 173 | */ 174 | TabberInternal.TabberSession.prototype.fromSync = function(syncObj) { 175 | // consoleDebugLog("Sess: "+JSON.stringify(syncObj)); 176 | for (var property in syncObj) { 177 | // save tabs 178 | if (property.startsWith('Tab_')) { 179 | this.tabs.push(syncObj[property]); 180 | } else { 181 | this[property] = syncObj[property]; 182 | } 183 | } 184 | this.numtabs = Number(this.tabs.length); 185 | }; 186 | 187 | /** 188 | * Return user-friendly session info (for debug/printing). 189 | * @return {string} 190 | */ 191 | TabberInternal.TabberSession.prototype.toString = function() { 192 | // consoleDebugLog("Sess: "+JSON.stringify(tabSession)); 193 | var output = this.description + ' (' + this.generation + ')\n'; 194 | output += 'Last update: ' + this.getTimeString() + '\n'; 195 | // debug print the tab titles 196 | output += tabsToString(this.tabs); 197 | return output; 198 | }; 199 | 200 | /** 201 | * Helper to print session info to console. 202 | * @param {string} label - Label to tag the output with. 203 | */ 204 | TabberInternal.TabberSession.prototype.printSession = function(label) { 205 | // consoleDebugLog("Sess: "+JSON.stringify(tabSession)); 206 | var tag = ' '; 207 | if (label) { 208 | tag = label + ' '; 209 | } 210 | consoleDebugLog(tag + 'Session: ' + this.toString()); 211 | }; 212 | 213 | 214 | /** 215 | * Tests for recognized property. 216 | * @param {string} prop - The property name to check. 217 | * @return {boolean} 218 | */ 219 | function isPropertyValid(prop) { 220 | if (!prop) { 221 | consoleDebugLog('No Proprty found'); 222 | return false; 223 | } 224 | // create a dummy session 225 | var sess = new TabberInternal.TabberSession(); 226 | 227 | // We only allow named tab element property setting 228 | if (prop.startsWith('Tab_')) return true; 229 | if (prop == 'tabs') return false; 230 | 231 | // All other props are ok. 232 | if (prop in sess) return true; 233 | 234 | return false; 235 | } 236 | 237 | /** 238 | * Tests for session validity. 239 | * @param {TabberInt.TabberSession} sess - The session to check. 240 | * @return {boolean} 241 | */ 242 | function isSessionValid(sess) { 243 | if (!sess) { 244 | consoleDebugLog('No Session found'); 245 | return false; 246 | } 247 | // validate the official properties of a session 248 | if (typeof sess.description != 'string') { 249 | consoleDebugLog('Session has bad description'); 250 | return false; 251 | } 252 | if (typeof sess.updateTime != 'number') { 253 | consoleDebugLog('Session has bad updateTime'); 254 | consoleDebugLog('Session updateTime type = ' + typeof sess.updateTime); 255 | consoleDebugLog( 256 | 'Session has updateTime: ' + JSON.stringify(sess.updateTime)); 257 | return false; 258 | } 259 | if (typeof sess.generation != 'number') { 260 | consoleDebugLog('Session has bad generation'); 261 | return false; 262 | } 263 | if (typeof sess.tabs != 'object') { 264 | consoleDebugLog('Session has bad tabs'); 265 | return false; 266 | } 267 | if (sess.tabs.length < 1) { 268 | consoleDebugLog('Session has NO tabs'); 269 | return false; 270 | } 271 | // check the critical tab props 272 | for (var t = 0; t < sess.tabs.length; t++) { 273 | // index 274 | if ((typeof sess.tabs[t].index != 'number') || (sess.tabs[t].index < 0)) { 275 | consoleDebugLog('Session tab ' + t + ' has bad index'); 276 | return false; 277 | } 278 | // url 279 | if (typeof sess.tabs[t].url != 'string') { 280 | consoleDebugLog('Session tab ' + t + ' has bad url'); 281 | return false; 282 | } 283 | // id 284 | if ((typeof sess.tabs[t].id != 'number') || (sess.tabs[t].id < 0)) { 285 | consoleDebugLog('Session tab ' + t + ' has bad id'); 286 | return false; 287 | } 288 | } 289 | return true; 290 | } 291 | 292 | /** 293 | * This tests whether there are any major or minor differences between the 294 | * local and remote sessions. If so, it returns an object describing the first 295 | * difference found. Major differences are checked first and no minor 296 | * differences are returned if any major difference is found. 297 | * @param {TabberInt.TabberSession} loc - Local session to compare. 298 | * @param {TabberInt.TabberSession} rem - Remote session to compare. 299 | * @return {TabberInt.type.TabDiff} 300 | */ 301 | function getSessionDiff(loc, rem) { 302 | consoleDebugLog('Checking for session diffs...'); 303 | /** @type {TabberInt.type.TabDiff} */ 304 | var result = {major: '', minor: '', err: false}; 305 | // debug print the sessions 306 | loc.printSession('Local'); 307 | rem.printSession('Remote'); 308 | 309 | // first make sure we have valid local and remote sessions 310 | if (loc.generation < 0) { 311 | result.major = 'Local browser not initialized yet'; 312 | result.err = true; 313 | consoleDebugLog(result.major); 314 | return result; 315 | } 316 | if ((!loc.tabs) || (loc.tabs.length < 1)) { 317 | result.major = 'No browser tabs found'; 318 | result.err = true; 319 | consoleDebugLog(result.major); 320 | // Mark the local session as uninitialized. 321 | loc.generation = -1; 322 | return result; 323 | } 324 | if (rem.generation < 0) { 325 | result.major = 'No saved session found'; 326 | result.err = true; 327 | consoleDebugLog(result.major); 328 | return result; 329 | } 330 | if ((!rem.tabs) || (rem.tabs.length < 1)) { 331 | result.major = 'No saved tabs found'; 332 | consoleDebugLog(result.major); 333 | // Mark the remote session as uninitialized. 334 | rem.generation = -1; 335 | return result; 336 | } 337 | 338 | var localOrder = 'Saved session has '; 339 | var tabDiff = getTabsetDiff(loc.tabs, rem.tabs); 340 | // Look for session differences to report. 341 | if (tabDiff.major) { 342 | result.major = localOrder + tabDiff.major; 343 | } else if (tabDiff.minor) { 344 | result.minor = localOrder + tabDiff.minor; 345 | } 346 | // If we have a diff, handle it here. 347 | if (result.major || result.minor) { 348 | // Make sure we don't have a gen tie. 349 | if (loc.generation == rem.generation) { 350 | loc.generation++; 351 | } 352 | // Report to caller. 353 | return result; 354 | } 355 | 356 | /* We didn't find any differences; silently fix any data inconsistencies */ 357 | 358 | // Check the generation. 359 | if (loc.generation != rem.generation) { 360 | // for generation mismatch, take the remote gen 361 | loc.generation = rem.generation; 362 | consoleDebugLog('Rebased local session to gen ' + loc.generation); 363 | } 364 | 365 | // Check last change time. 366 | if (loc.updateTime != rem.updateTime) { 367 | // for updateTime mismatch, take the later time 368 | loc.updateTime = rem.updateTime = Math.max(loc.updateTime, 369 | rem.updateTime); 370 | consoleDebugLog('Resolved update time: ' + loc.getTimeString()); 371 | } 372 | 373 | consoleDebugLog('Active and saved sessions are in sync'); 374 | return result; 375 | } 376 | 377 | -------------------------------------------------------------------------------- /src/js/tabs.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2018 Google LLC 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | https://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | /** 18 | * @fileoverview 19 | * tabs.js - Provides functions for working with Tab objects. 20 | */ 21 | 22 | /** 23 | * This function simply counts the number of windowids in a set of tabs. 24 | * @param {Array} tabs - Set of tabs to examine. 25 | * @return {number} 26 | */ 27 | function getTabsetWinCount(tabs) { 28 | var winset = {}; 29 | for (var t = 0; t < tabs.length; t++) { 30 | /** type {TabberInt.type.Tab} */ 31 | var tab = tabs[t]; 32 | winset[tab.windowId] = true; 33 | } 34 | return Object.keys(winset).length; 35 | } 36 | 37 | /** 38 | * Examine two tabs and report on the first difference found (if any). This 39 | * looks first for a major difference, and if not found, a minor difference. 40 | * Returns an object with 'minor' and 'major' keys, whos values are strings 41 | * describing the first difference found. Only one difference is indicated. 42 | * @param {Object} tab1 - First tab to compare. 43 | * @param {Object} tab2 - Second tab to compare. 44 | * @return {TabberInt.type.TabDiff} 45 | */ 46 | function getTabsDiff(tab1, tab2) { 47 | /** @type {TabberInt.type.TabDiff} */ 48 | var result = {major: '', minor: '', err: false}; 49 | // Look for a major difference. 50 | // First, compare URLs. 51 | if (tab1.url != tab2.url) { 52 | consoleDebugLog( 53 | 'Tab \'' + tab1.title + '\' and tab \'' + tab2.title + 54 | '\' have different URLs'); 55 | consoleDebugLog('Tab1: ' + tab1.url); 56 | consoleDebugLog('Tab2: ' + tab2.url); 57 | result.major = 'different URLs'; 58 | return result; 59 | } 60 | // Check for a minor diff: active state 61 | if (tab1.active != tab2.active) { 62 | consoleDebugLog( 63 | 'Tab \'' + tab1.title + '\' and tab \'' + tab2.title + 64 | '\' have different active state'); 65 | result.minor = 'a different active tab.'; 66 | } 67 | return result; 68 | } 69 | 70 | /** 71 | * This is the function that compares sets of tabs from two sessions to find 72 | * any major or minor differences between them. Results are always given from 73 | * the perspective of the second set of tabs. 74 | * @param {Array} tabs1 - First set of tabs to compare. 75 | * @param {Array} tabs2 - Second set of tabs to compare. 76 | * @return {TabberInt.type.TabDiff} 77 | */ 78 | function getTabsetDiff(tabs1, tabs2) { 79 | /** @type {TabberInt.type.TabDiff} */ 80 | var dif = {major: '', minor: '', err: false}; 81 | // First see if sets have same number of tabs. 82 | if (tabs1.length != tabs2.length) { 83 | if (tabs1.length > tabs2.length) { 84 | dif.major = (tabs1.length - tabs2.length) + ' fewer tabs'; 85 | } else { 86 | dif.major = (tabs2.length - tabs1.length) + ' more tabs'; 87 | } 88 | return dif; 89 | } 90 | // Check if sets have same number of windows. 91 | var wincnt1 = getTabsetWinCount(tabs1); 92 | var wincnt2 = getTabsetWinCount(tabs2); 93 | if (wincnt1 != wincnt2) { 94 | if (wincnt1 > wincnt2) { 95 | dif.major = (wincnt1 - wincnt2) + ' fewer windows'; 96 | } else { 97 | dif.major = (wincnt2 - wincnt1) + ' more windows'; 98 | } 99 | return dif; 100 | } 101 | 102 | // Tab sets have same number of tabs/windows, so look closer 103 | for (var t = 0; t < tabs1.length; t++) { 104 | var diff = getTabsDiff(tabs1[t], tabs2[t]); 105 | // If we found a major difference, we can stop here 106 | if (diff.major) { 107 | return diff; 108 | } 109 | // Save the first minor diff (in case we don't find any major difs) 110 | if (diff.minor && !dif.minor) { 111 | dif.minor = diff.minor; 112 | } 113 | } 114 | return dif; 115 | } 116 | 117 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tabber Multi-machine Tab Manager", 3 | "short_name": "Tabber", 4 | "description": "Save and restore your Chrome browser tabs across machines.", 5 | "version": "1.6", 6 | "manifest_version": 2, 7 | "icons": { 8 | "16": "images/tabber_16.png", 9 | "32": "images/tabber_32.png", 10 | "48": "images/tabber_48.png", 11 | "128": "images/tabber_128.png" 12 | }, 13 | "content_security_policy": "script-src 'self'; object-src 'self'", 14 | "browser_action": { 15 | "default_icon": { 16 | "19": "images/tabber_yellow_19.png", 17 | "38": "images/tabber_yellow_38.png" 18 | }, 19 | "default_popup": "static/popup.html", 20 | "default_title": "Click here to manage browser tabs" 21 | }, 22 | "background": { 23 | "scripts": [ 24 | "js/tabber_ext.js" 25 | ] 26 | }, 27 | "permissions": [ 28 | "storage", 29 | "activeTab", 30 | "tabs" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /src/static/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | Tabber 24 | 36 | 37 | 38 | 39 |
40 |
41 | Current Session 42 |
43 |
44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 |
54 |
55 |
56 | Choose when Tabber should save or restore 57 | 58 | Manual
59 |
60 | 61 | Startup Only
62 |
63 | 64 | Auto-Save
65 |
66 | 67 | Fully Automatic
68 |
69 |
70 |
71 |
72 | 73 | Debug mode
74 |
75 | 76 | 77 | 78 | --------------------------------------------------------------------------------