addons/firefox-addons/set-aside

changeset 0:d13d59494613

Initial revision
author Guido Berhoerster <guido+set-aside@berhoerster.name>
date Sat Nov 17 10:44:16 2018 +0100 (18 months ago)
parents
children b0827360b8e4
files COPYING Makefile NEWS README _locales/de/messages.json _locales/en/messages.json background.js icons/set-aside-action-dark.svg icons/set-aside-action-light.svg icons/set-aside-sidebar.svg icons/set-aside.svg manifest.json.in sidebar/images/defaultFavicon.svg sidebar/images/remove.svg sidebar/images/thumbnail-placeholder.svg sidebar/js/tab-collection-manager.js sidebar/style/photon-colors.css sidebar/style/tab-collection-manager.css sidebar/tab-collection-manager.html
line diff
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/COPYING	Sat Nov 17 10:44:16 2018 +0100
     1.3 @@ -0,0 +1,373 @@
     1.4 +Mozilla Public License Version 2.0
     1.5 +==================================
     1.6 +
     1.7 +1. Definitions
     1.8 +--------------
     1.9 +
    1.10 +1.1. "Contributor"
    1.11 +    means each individual or legal entity that creates, contributes to
    1.12 +    the creation of, or owns Covered Software.
    1.13 +
    1.14 +1.2. "Contributor Version"
    1.15 +    means the combination of the Contributions of others (if any) used
    1.16 +    by a Contributor and that particular Contributor's Contribution.
    1.17 +
    1.18 +1.3. "Contribution"
    1.19 +    means Covered Software of a particular Contributor.
    1.20 +
    1.21 +1.4. "Covered Software"
    1.22 +    means Source Code Form to which the initial Contributor has attached
    1.23 +    the notice in Exhibit A, the Executable Form of such Source Code
    1.24 +    Form, and Modifications of such Source Code Form, in each case
    1.25 +    including portions thereof.
    1.26 +
    1.27 +1.5. "Incompatible With Secondary Licenses"
    1.28 +    means
    1.29 +
    1.30 +    (a) that the initial Contributor has attached the notice described
    1.31 +        in Exhibit B to the Covered Software; or
    1.32 +
    1.33 +    (b) that the Covered Software was made available under the terms of
    1.34 +        version 1.1 or earlier of the License, but not also under the
    1.35 +        terms of a Secondary License.
    1.36 +
    1.37 +1.6. "Executable Form"
    1.38 +    means any form of the work other than Source Code Form.
    1.39 +
    1.40 +1.7. "Larger Work"
    1.41 +    means a work that combines Covered Software with other material, in 
    1.42 +    a separate file or files, that is not Covered Software.
    1.43 +
    1.44 +1.8. "License"
    1.45 +    means this document.
    1.46 +
    1.47 +1.9. "Licensable"
    1.48 +    means having the right to grant, to the maximum extent possible,
    1.49 +    whether at the time of the initial grant or subsequently, any and
    1.50 +    all of the rights conveyed by this License.
    1.51 +
    1.52 +1.10. "Modifications"
    1.53 +    means any of the following:
    1.54 +
    1.55 +    (a) any file in Source Code Form that results from an addition to,
    1.56 +        deletion from, or modification of the contents of Covered
    1.57 +        Software; or
    1.58 +
    1.59 +    (b) any new file in Source Code Form that contains any Covered
    1.60 +        Software.
    1.61 +
    1.62 +1.11. "Patent Claims" of a Contributor
    1.63 +    means any patent claim(s), including without limitation, method,
    1.64 +    process, and apparatus claims, in any patent Licensable by such
    1.65 +    Contributor that would be infringed, but for the grant of the
    1.66 +    License, by the making, using, selling, offering for sale, having
    1.67 +    made, import, or transfer of either its Contributions or its
    1.68 +    Contributor Version.
    1.69 +
    1.70 +1.12. "Secondary License"
    1.71 +    means either the GNU General Public License, Version 2.0, the GNU
    1.72 +    Lesser General Public License, Version 2.1, the GNU Affero General
    1.73 +    Public License, Version 3.0, or any later versions of those
    1.74 +    licenses.
    1.75 +
    1.76 +1.13. "Source Code Form"
    1.77 +    means the form of the work preferred for making modifications.
    1.78 +
    1.79 +1.14. "You" (or "Your")
    1.80 +    means an individual or a legal entity exercising rights under this
    1.81 +    License. For legal entities, "You" includes any entity that
    1.82 +    controls, is controlled by, or is under common control with You. For
    1.83 +    purposes of this definition, "control" means (a) the power, direct
    1.84 +    or indirect, to cause the direction or management of such entity,
    1.85 +    whether by contract or otherwise, or (b) ownership of more than
    1.86 +    fifty percent (50%) of the outstanding shares or beneficial
    1.87 +    ownership of such entity.
    1.88 +
    1.89 +2. License Grants and Conditions
    1.90 +--------------------------------
    1.91 +
    1.92 +2.1. Grants
    1.93 +
    1.94 +Each Contributor hereby grants You a world-wide, royalty-free,
    1.95 +non-exclusive license:
    1.96 +
    1.97 +(a) under intellectual property rights (other than patent or trademark)
    1.98 +    Licensable by such Contributor to use, reproduce, make available,
    1.99 +    modify, display, perform, distribute, and otherwise exploit its
   1.100 +    Contributions, either on an unmodified basis, with Modifications, or
   1.101 +    as part of a Larger Work; and
   1.102 +
   1.103 +(b) under Patent Claims of such Contributor to make, use, sell, offer
   1.104 +    for sale, have made, import, and otherwise transfer either its
   1.105 +    Contributions or its Contributor Version.
   1.106 +
   1.107 +2.2. Effective Date
   1.108 +
   1.109 +The licenses granted in Section 2.1 with respect to any Contribution
   1.110 +become effective for each Contribution on the date the Contributor first
   1.111 +distributes such Contribution.
   1.112 +
   1.113 +2.3. Limitations on Grant Scope
   1.114 +
   1.115 +The licenses granted in this Section 2 are the only rights granted under
   1.116 +this License. No additional rights or licenses will be implied from the
   1.117 +distribution or licensing of Covered Software under this License.
   1.118 +Notwithstanding Section 2.1(b) above, no patent license is granted by a
   1.119 +Contributor:
   1.120 +
   1.121 +(a) for any code that a Contributor has removed from Covered Software;
   1.122 +    or
   1.123 +
   1.124 +(b) for infringements caused by: (i) Your and any other third party's
   1.125 +    modifications of Covered Software, or (ii) the combination of its
   1.126 +    Contributions with other software (except as part of its Contributor
   1.127 +    Version); or
   1.128 +
   1.129 +(c) under Patent Claims infringed by Covered Software in the absence of
   1.130 +    its Contributions.
   1.131 +
   1.132 +This License does not grant any rights in the trademarks, service marks,
   1.133 +or logos of any Contributor (except as may be necessary to comply with
   1.134 +the notice requirements in Section 3.4).
   1.135 +
   1.136 +2.4. Subsequent Licenses
   1.137 +
   1.138 +No Contributor makes additional grants as a result of Your choice to
   1.139 +distribute the Covered Software under a subsequent version of this
   1.140 +License (see Section 10.2) or under the terms of a Secondary License (if
   1.141 +permitted under the terms of Section 3.3).
   1.142 +
   1.143 +2.5. Representation
   1.144 +
   1.145 +Each Contributor represents that the Contributor believes its
   1.146 +Contributions are its original creation(s) or it has sufficient rights
   1.147 +to grant the rights to its Contributions conveyed by this License.
   1.148 +
   1.149 +2.6. Fair Use
   1.150 +
   1.151 +This License is not intended to limit any rights You have under
   1.152 +applicable copyright doctrines of fair use, fair dealing, or other
   1.153 +equivalents.
   1.154 +
   1.155 +2.7. Conditions
   1.156 +
   1.157 +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
   1.158 +in Section 2.1.
   1.159 +
   1.160 +3. Responsibilities
   1.161 +-------------------
   1.162 +
   1.163 +3.1. Distribution of Source Form
   1.164 +
   1.165 +All distribution of Covered Software in Source Code Form, including any
   1.166 +Modifications that You create or to which You contribute, must be under
   1.167 +the terms of this License. You must inform recipients that the Source
   1.168 +Code Form of the Covered Software is governed by the terms of this
   1.169 +License, and how they can obtain a copy of this License. You may not
   1.170 +attempt to alter or restrict the recipients' rights in the Source Code
   1.171 +Form.
   1.172 +
   1.173 +3.2. Distribution of Executable Form
   1.174 +
   1.175 +If You distribute Covered Software in Executable Form then:
   1.176 +
   1.177 +(a) such Covered Software must also be made available in Source Code
   1.178 +    Form, as described in Section 3.1, and You must inform recipients of
   1.179 +    the Executable Form how they can obtain a copy of such Source Code
   1.180 +    Form by reasonable means in a timely manner, at a charge no more
   1.181 +    than the cost of distribution to the recipient; and
   1.182 +
   1.183 +(b) You may distribute such Executable Form under the terms of this
   1.184 +    License, or sublicense it under different terms, provided that the
   1.185 +    license for the Executable Form does not attempt to limit or alter
   1.186 +    the recipients' rights in the Source Code Form under this License.
   1.187 +
   1.188 +3.3. Distribution of a Larger Work
   1.189 +
   1.190 +You may create and distribute a Larger Work under terms of Your choice,
   1.191 +provided that You also comply with the requirements of this License for
   1.192 +the Covered Software. If the Larger Work is a combination of Covered
   1.193 +Software with a work governed by one or more Secondary Licenses, and the
   1.194 +Covered Software is not Incompatible With Secondary Licenses, this
   1.195 +License permits You to additionally distribute such Covered Software
   1.196 +under the terms of such Secondary License(s), so that the recipient of
   1.197 +the Larger Work may, at their option, further distribute the Covered
   1.198 +Software under the terms of either this License or such Secondary
   1.199 +License(s).
   1.200 +
   1.201 +3.4. Notices
   1.202 +
   1.203 +You may not remove or alter the substance of any license notices
   1.204 +(including copyright notices, patent notices, disclaimers of warranty,
   1.205 +or limitations of liability) contained within the Source Code Form of
   1.206 +the Covered Software, except that You may alter any license notices to
   1.207 +the extent required to remedy known factual inaccuracies.
   1.208 +
   1.209 +3.5. Application of Additional Terms
   1.210 +
   1.211 +You may choose to offer, and to charge a fee for, warranty, support,
   1.212 +indemnity or liability obligations to one or more recipients of Covered
   1.213 +Software. However, You may do so only on Your own behalf, and not on
   1.214 +behalf of any Contributor. You must make it absolutely clear that any
   1.215 +such warranty, support, indemnity, or liability obligation is offered by
   1.216 +You alone, and You hereby agree to indemnify every Contributor for any
   1.217 +liability incurred by such Contributor as a result of warranty, support,
   1.218 +indemnity or liability terms You offer. You may include additional
   1.219 +disclaimers of warranty and limitations of liability specific to any
   1.220 +jurisdiction.
   1.221 +
   1.222 +4. Inability to Comply Due to Statute or Regulation
   1.223 +---------------------------------------------------
   1.224 +
   1.225 +If it is impossible for You to comply with any of the terms of this
   1.226 +License with respect to some or all of the Covered Software due to
   1.227 +statute, judicial order, or regulation then You must: (a) comply with
   1.228 +the terms of this License to the maximum extent possible; and (b)
   1.229 +describe the limitations and the code they affect. Such description must
   1.230 +be placed in a text file included with all distributions of the Covered
   1.231 +Software under this License. Except to the extent prohibited by statute
   1.232 +or regulation, such description must be sufficiently detailed for a
   1.233 +recipient of ordinary skill to be able to understand it.
   1.234 +
   1.235 +5. Termination
   1.236 +--------------
   1.237 +
   1.238 +5.1. The rights granted under this License will terminate automatically
   1.239 +if You fail to comply with any of its terms. However, if You become
   1.240 +compliant, then the rights granted under this License from a particular
   1.241 +Contributor are reinstated (a) provisionally, unless and until such
   1.242 +Contributor explicitly and finally terminates Your grants, and (b) on an
   1.243 +ongoing basis, if such Contributor fails to notify You of the
   1.244 +non-compliance by some reasonable means prior to 60 days after You have
   1.245 +come back into compliance. Moreover, Your grants from a particular
   1.246 +Contributor are reinstated on an ongoing basis if such Contributor
   1.247 +notifies You of the non-compliance by some reasonable means, this is the
   1.248 +first time You have received notice of non-compliance with this License
   1.249 +from such Contributor, and You become compliant prior to 30 days after
   1.250 +Your receipt of the notice.
   1.251 +
   1.252 +5.2. If You initiate litigation against any entity by asserting a patent
   1.253 +infringement claim (excluding declaratory judgment actions,
   1.254 +counter-claims, and cross-claims) alleging that a Contributor Version
   1.255 +directly or indirectly infringes any patent, then the rights granted to
   1.256 +You by any and all Contributors for the Covered Software under Section
   1.257 +2.1 of this License shall terminate.
   1.258 +
   1.259 +5.3. In the event of termination under Sections 5.1 or 5.2 above, all
   1.260 +end user license agreements (excluding distributors and resellers) which
   1.261 +have been validly granted by You or Your distributors under this License
   1.262 +prior to termination shall survive termination.
   1.263 +
   1.264 +************************************************************************
   1.265 +*                                                                      *
   1.266 +*  6. Disclaimer of Warranty                                           *
   1.267 +*  -------------------------                                           *
   1.268 +*                                                                      *
   1.269 +*  Covered Software is provided under this License on an "as is"       *
   1.270 +*  basis, without warranty of any kind, either expressed, implied, or  *
   1.271 +*  statutory, including, without limitation, warranties that the       *
   1.272 +*  Covered Software is free of defects, merchantable, fit for a        *
   1.273 +*  particular purpose or non-infringing. The entire risk as to the     *
   1.274 +*  quality and performance of the Covered Software is with You.        *
   1.275 +*  Should any Covered Software prove defective in any respect, You     *
   1.276 +*  (not any Contributor) assume the cost of any necessary servicing,   *
   1.277 +*  repair, or correction. This disclaimer of warranty constitutes an   *
   1.278 +*  essential part of this License. No use of any Covered Software is   *
   1.279 +*  authorized under this License except under this disclaimer.         *
   1.280 +*                                                                      *
   1.281 +************************************************************************
   1.282 +
   1.283 +************************************************************************
   1.284 +*                                                                      *
   1.285 +*  7. Limitation of Liability                                          *
   1.286 +*  --------------------------                                          *
   1.287 +*                                                                      *
   1.288 +*  Under no circumstances and under no legal theory, whether tort      *
   1.289 +*  (including negligence), contract, or otherwise, shall any           *
   1.290 +*  Contributor, or anyone who distributes Covered Software as          *
   1.291 +*  permitted above, be liable to You for any direct, indirect,         *
   1.292 +*  special, incidental, or consequential damages of any character      *
   1.293 +*  including, without limitation, damages for lost profits, loss of    *
   1.294 +*  goodwill, work stoppage, computer failure or malfunction, or any    *
   1.295 +*  and all other commercial damages or losses, even if such party      *
   1.296 +*  shall have been informed of the possibility of such damages. This   *
   1.297 +*  limitation of liability shall not apply to liability for death or   *
   1.298 +*  personal injury resulting from such party's negligence to the       *
   1.299 +*  extent applicable law prohibits such limitation. Some               *
   1.300 +*  jurisdictions do not allow the exclusion or limitation of           *
   1.301 +*  incidental or consequential damages, so this exclusion and          *
   1.302 +*  limitation may not apply to You.                                    *
   1.303 +*                                                                      *
   1.304 +************************************************************************
   1.305 +
   1.306 +8. Litigation
   1.307 +-------------
   1.308 +
   1.309 +Any litigation relating to this License may be brought only in the
   1.310 +courts of a jurisdiction where the defendant maintains its principal
   1.311 +place of business and such litigation shall be governed by laws of that
   1.312 +jurisdiction, without reference to its conflict-of-law provisions.
   1.313 +Nothing in this Section shall prevent a party's ability to bring
   1.314 +cross-claims or counter-claims.
   1.315 +
   1.316 +9. Miscellaneous
   1.317 +----------------
   1.318 +
   1.319 +This License represents the complete agreement concerning the subject
   1.320 +matter hereof. If any provision of this License is held to be
   1.321 +unenforceable, such provision shall be reformed only to the extent
   1.322 +necessary to make it enforceable. Any law or regulation which provides
   1.323 +that the language of a contract shall be construed against the drafter
   1.324 +shall not be used to construe this License against a Contributor.
   1.325 +
   1.326 +10. Versions of the License
   1.327 +---------------------------
   1.328 +
   1.329 +10.1. New Versions
   1.330 +
   1.331 +Mozilla Foundation is the license steward. Except as provided in Section
   1.332 +10.3, no one other than the license steward has the right to modify or
   1.333 +publish new versions of this License. Each version will be given a
   1.334 +distinguishing version number.
   1.335 +
   1.336 +10.2. Effect of New Versions
   1.337 +
   1.338 +You may distribute the Covered Software under the terms of the version
   1.339 +of the License under which You originally received the Covered Software,
   1.340 +or under the terms of any subsequent version published by the license
   1.341 +steward.
   1.342 +
   1.343 +10.3. Modified Versions
   1.344 +
   1.345 +If you create software not governed by this License, and you want to
   1.346 +create a new license for such software, you may create and use a
   1.347 +modified version of this License if you rename the license and remove
   1.348 +any references to the name of the license steward (except to note that
   1.349 +such modified license differs from this License).
   1.350 +
   1.351 +10.4. Distributing Source Code Form that is Incompatible With Secondary
   1.352 +Licenses
   1.353 +
   1.354 +If You choose to distribute Source Code Form that is Incompatible With
   1.355 +Secondary Licenses under the terms of this version of the License, the
   1.356 +notice described in Exhibit B of this License must be attached.
   1.357 +
   1.358 +Exhibit A - Source Code Form License Notice
   1.359 +-------------------------------------------
   1.360 +
   1.361 +  This Source Code Form is subject to the terms of the Mozilla Public
   1.362 +  License, v. 2.0. If a copy of the MPL was not distributed with this
   1.363 +  file, You can obtain one at http://mozilla.org/MPL/2.0/.
   1.364 +
   1.365 +If it is not possible or desirable to put the notice in a particular
   1.366 +file, then You may include the notice in a location (such as a LICENSE
   1.367 +file in a relevant directory) where a recipient would be likely to look
   1.368 +for such a notice.
   1.369 +
   1.370 +You may add additional accurate notices of copyright ownership.
   1.371 +
   1.372 +Exhibit B - "Incompatible With Secondary Licenses" Notice
   1.373 +---------------------------------------------------------
   1.374 +
   1.375 +  This Source Code Form is "Incompatible With Secondary Licenses", as
   1.376 +  defined by the Mozilla Public License, v. 2.0.
     2.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     2.2 +++ b/Makefile	Sat Nov 17 10:44:16 2018 +0100
     2.3 @@ -0,0 +1,82 @@
     2.4 +#
     2.5 +# Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
     2.6 +#
     2.7 +# Permission is hereby granted, free of charge, to any person obtaining
     2.8 +# a copy of this software and associated documentation files (the
     2.9 +# "Software"), to deal in the Software without restriction, including
    2.10 +# without limitation the rights to use, copy, modify, merge, publish,
    2.11 +# distribute, sublicense, and/or sell copies of the Software, and to
    2.12 +# permit persons to whom the Software is furnished to do so, subject to
    2.13 +# the following conditions:
    2.14 +#
    2.15 +# The above copyright notice and this permission notice shall be included
    2.16 +# in all copies or substantial portions of the Software.
    2.17 +#
    2.18 +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
    2.19 +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
    2.20 +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
    2.21 +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
    2.22 +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
    2.23 +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
    2.24 +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    2.25 +#
    2.26 +
    2.27 +NAME =		set-aside
    2.28 +VERSION =	1
    2.29 +EXT_NAME =	$(subst -,_,$(NAME))-$(VERSION)
    2.30 +
    2.31 +INKSCAPE := 	inkscape
    2.32 +INFOZIP :=	zip
    2.33 +SED :=		sed
    2.34 +
    2.35 +BITMAP_ICONS =	icons/set-aside-16.png \
    2.36 +		icons/set-aside-32.png \
    2.37 +		icons/set-aside-48.png \
    2.38 +		icons/set-aside-96.png \
    2.39 +		icons/set-aside-action-dark-16.png \
    2.40 +		icons/set-aside-action-dark-32.png \
    2.41 +		icons/set-aside-action-light-16.png \
    2.42 +		icons/set-aside-action-light-32.png \
    2.43 +		icons/set-aside-sidebar-16.png \
    2.44 +		icons/set-aside-sidebar-32.png
    2.45 +DIST_FILES =	COPYING \
    2.46 +		NEWS \
    2.47 +		README \
    2.48 +		background.js \
    2.49 +		manifest.json \
    2.50 +		sidebar/images/defaultFavicon.svg \
    2.51 +		sidebar/images/remove.svg \
    2.52 +		sidebar/images/thumbnail-placeholder.svg \
    2.53 +		sidebar/js/tab-collection-manager.js \
    2.54 +		sidebar/style/photon-colors.css \
    2.55 +		sidebar/style/tab-collection-manager.css \
    2.56 +		sidebar/tab-collection-manager.html \
    2.57 +		$(wildcard _locales/*/messages.json) \
    2.58 +		$(BITMAP_ICONS)
    2.59 +
    2.60 +.DEFAULT_TARGET = all
    2.61 +
    2.62 +.PHONY: all extension clean clobber
    2.63 +
    2.64 +all: extension
    2.65 +
    2.66 +extension: $(EXT_NAME).zip
    2.67 +
    2.68 +$(EXT_NAME).zip: $(DIST_FILES)
    2.69 +	$(INFOZIP) $@ $^
    2.70 +
    2.71 +define generate-icon-rule
    2.72 +$1: $(1:%-$(lastword $(subst -, ,$1))=%.svg)
    2.73 +	size=$(lastword $(subst -, ,$(basename $1))); \
    2.74 +	    $(INKSCAPE) -w $$$${size} -h $$$${size} -e $$@ $$<
    2.75 +endef
    2.76 +
    2.77 +$(foreach icon,$(BITMAP_ICONS),$(eval $(call generate-icon-rule,$(icon))))
    2.78 +
    2.79 +manifest.json: manifest.json.in
    2.80 +	$(SED) 's|@VERSION@|$(VERSION)|g' $< >$@
    2.81 +
    2.82 +clean:
    2.83 +	-rm -f $(BITMAP_ICONS) manifest.json
    2.84 +
    2.85 +clobber: clean
     3.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     3.2 +++ b/NEWS	Sat Nov 17 10:44:16 2018 +0100
     3.3 @@ -0,0 +1,2 @@
     3.4 +News
     3.5 +====
     4.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     4.2 +++ b/README	Sat Nov 17 10:44:16 2018 +0100
     4.3 @@ -0,0 +1,56 @@
     4.4 +Set Aside
     4.5 +=========
     4.6 +
     4.7 +Set Aside is a Firefox Addon which can quickly set aside open tabs from the
     4.8 +current window and restore them later.
     4.9 +
    4.10 +Usage
    4.11 +-----
    4.12 +
    4.13 +Press the extension's button in the tab bar in order to set all currently open
    4.14 +tabs aside.  Note that tabs which are set aside are closed, this may lead to a
    4.15 +loss of data if e.g. there is unsubmitted form data.  Tabs with privileged
    4.16 +protocols such as file:, moz-extension:, or about: cannot be set aside and
    4.17 +will be left open because the extension would not be able to open them again
    4.18 +due to browser security policy.
    4.19 +
    4.20 +Tabs which were set aside can be managed via the sidebar which shows the
    4.21 +collections of tabs in the order of creation, including the number of tabs
    4.22 +belonging to the collection, its creation date and time, as well as thumbnail
    4.23 +previews of each tab.  Thumbnail previews are not available for tabs which had
    4.24 +been discarded before being set aside or tabs synced from another device.
    4.25 +
    4.26 +Clicking on "Restore Tabs" will restore all tabs of the collection in the
    4.27 +current window and subsequently remove the collection from the sidebar.
    4.28 +Clicking on a tab thumbnail will restore that particular tab and subsequently
    4.29 +remove it from the collection.
    4.30 +
    4.31 +Pressing the cross button above the tab thumbnails will remove all tabs
    4.32 +belonging to the collection.  Pressing the cross button on a tab thumbnail
    4.33 +will remove that particular tab from the collection.
    4.34 +
    4.35 +Contact
    4.36 +-------
    4.37 +
    4.38 +Please send any feedback, translations or bug reports via email to
    4.39 +<guido+set-aside@berhoerster.name>
    4.40 +
    4.41 +Bug Reports
    4.42 +-----------
    4.43 +
    4.44 +When sending bug reports, please always mention the exact version of the addon
    4.45 +with which the issue occurs as well as the version of Firefox and the operating
    4.46 +system you are using and make sure that you provide sufficient information to
    4.47 +reproduce the issue and include any error messages.
    4.48 +
    4.49 +License
    4.50 +-------
    4.51 +
    4.52 +Except otherwise noted, all files are Copyright (C) 2018 Guido Berhoerster and
    4.53 +distributed under the following license terms:
    4.54 +
    4.55 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    4.56 +
    4.57 +This Source Code Form is subject to the terms of the Mozilla Public
    4.58 +License, v. 2.0. If a copy of the MPL was not distributed with this
    4.59 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
     5.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     5.2 +++ b/_locales/de/messages.json	Sat Nov 17 10:44:16 2018 +0100
     5.3 @@ -0,0 +1,52 @@
     5.4 +{
     5.5 +    "extensionName": {
     5.6 +        "message": "Set Aside",
     5.7 +        "description": "Name of the extension."
     5.8 +    },
     5.9 +    "extensionDescription": {
    5.10 +        "message": "Speichert offene Tabs in einer Sammlung für später.",
    5.11 +        "description": "Description of the extension."
    5.12 +    },
    5.13 +    "defaultBrowserActionTitle": {
    5.14 +        "message": "Speichere diese Tabs für später",
    5.15 +        "description": "Title of the browser action button."
    5.16 +    },
    5.17 +    "defaultSidebarTitle": {
    5.18 +        "message": "Für später gespeicherte Tabs",
    5.19 +        "description": "Title of the sidebar action."
    5.20 +    },
    5.21 +    "showTabsMenuItem": {
    5.22 +        "message": "Für später gespeicherte Tabs zeigen",
    5.23 +        "description": "Title of the menu item for opening the sidebar action for managing tabs which were set aside."
    5.24 +    },
    5.25 +    "emptySidebarMessage": {
    5.26 +        "message": "Noch nichts",
    5.27 +        "description": "Message shown in the sidebar if there are no tab collections yet."
    5.28 +    },
    5.29 +    "collectionTitle": {
    5.30 +        "message": "$NUMBER$ Tab(s)",
    5.31 +        "description": "Title of the collection containing the number of tabs, the number placholder may refer to one or more tabs.",
    5.32 +        "placeholders": {
    5.33 +            "number": {
    5.34 +                "content": "$1",
    5.35 +                "example": "42"
    5.36 +            }
    5.37 +        }
    5.38 +    },
    5.39 +    "restoreTabsButtonTitle": {
    5.40 +        "message": "Tabs Wiederherstellen",
    5.41 +        "description": "Title of the button for restoring a tab collection."
    5.42 +    },
    5.43 +    "removeTabsButtonTitle": {
    5.44 +        "message": "Tabs Entfernen",
    5.45 +        "description": "Title of the button for removing a tab collection."
    5.46 +    },
    5.47 +    "removeTabTitle": {
    5.48 +        "message": "Tab Entfernen",
    5.49 +        "description": "Title of the button for removing a tab from a collection."
    5.50 +    },
    5.51 +    "incognitoModeMessage": {
    5.52 +        "message": "Privates Surfen",
    5.53 +        "description": "Message shown in the sidebar when in incognito mode."
    5.54 +    }
    5.55 +}
     6.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     6.2 +++ b/_locales/en/messages.json	Sat Nov 17 10:44:16 2018 +0100
     6.3 @@ -0,0 +1,52 @@
     6.4 +{
     6.5 +    "extensionName": {
     6.6 +        "message": "Set Aside",
     6.7 +        "description": "Name of the extension."
     6.8 +    },
     6.9 +    "extensionDescription": {
    6.10 +        "message": "Set open tabs aside in a collection and restore them later.",
    6.11 +        "description": "Description of the extension."
    6.12 +    },
    6.13 +    "defaultBrowserActionTitle": {
    6.14 +        "message": "Set these Tabs aside",
    6.15 +        "description": "Title of the browser action button."
    6.16 +    },
    6.17 +    "defaultSidebarTitle": {
    6.18 +        "message": "Tabs You've Set Aside",
    6.19 +        "description": "Title of the sidebar action."
    6.20 +    },
    6.21 +    "showTabsMenuItem": {
    6.22 +        "message": "Show Tabs You've Set Aside",
    6.23 +        "description": "Title of the menu item for opening the sidebar action for managing tabs which were set aside."
    6.24 +    },
    6.25 +    "emptySidebarMessage": {
    6.26 +        "message": "Nothing here yet",
    6.27 +        "description": "Message shown in the sidebar if there are no tab collections yet."
    6.28 +    },
    6.29 +    "collectionTitle": {
    6.30 +        "message": "$NUMBER$ Tab(s)",
    6.31 +        "description": "Title of the collection containing the number of tabs, the number placholder may refer to one or more tabs.",
    6.32 +        "placeholders": {
    6.33 +            "number": {
    6.34 +                "content": "$1",
    6.35 +                "example": "42"
    6.36 +            }
    6.37 +        }
    6.38 +    },
    6.39 +    "restoreTabsButtonTitle": {
    6.40 +        "message": "Restore Tabs",
    6.41 +        "description": "Title of the button for restoring a tab collection."
    6.42 +    },
    6.43 +    "removeTabsButtonTitle": {
    6.44 +        "message": "Remove Tabs",
    6.45 +        "description": "Title of the button for removing a tab collection."
    6.46 +    },
    6.47 +    "removeTabTitle": {
    6.48 +        "message": "Remove Tab",
    6.49 +        "description": "Title of the button for removing a tab from a collection."
    6.50 +    },
    6.51 +    "incognitoModeMessage": {
    6.52 +        "message": "Private Browsing",
    6.53 +        "description": "Message shown in the sidebar when in incognito mode."
    6.54 +    }
    6.55 +}
     7.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     7.2 +++ b/background.js	Sat Nov 17 10:44:16 2018 +0100
     7.3 @@ -0,0 +1,425 @@
     7.4 +/*
     7.5 + * Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
     7.6 + *
     7.7 + * This Source Code Form is subject to the terms of the Mozilla Public
     7.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     7.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
    7.10 + */
    7.11 +
    7.12 +'use strict';
    7.13 +
    7.14 +const SUPPORTED_PROTOCOLS = ['https:', 'http:', 'ftp:'];
    7.15 +const GROUP_KEY_RE = /^collection:[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$/;
    7.16 +const FIREFOX_VERSION_RE = /^(\d+(?:\.\d+)*)(?:([ab]|pre)(\d+))?$/;
    7.17 +const FIREFOX_RELEASE_TYPES = {
    7.18 +    'a': 'alpha',
    7.19 +    'b': 'beta',
    7.20 +    'pre': 'prerelease',
    7.21 +    '': ''
    7.22 +}
    7.23 +const THUMBNAIL_WIDTH = 224;
    7.24 +const THUMBNAIL_HEIGHT = 128;
    7.25 +
    7.26 +var tabCollectionsProxy;
    7.27 +
    7.28 +function generateUuidV4String() {
    7.29 +    let uuid = new Uint8Array(16);
    7.30 +    window.crypto.getRandomValues(uuid);
    7.31 +    uuid[6] = (uuid[6] & 0x0f) | 0x40;
    7.32 +    uuid[8] = (uuid[8] & 0x3f) | 0x80;
    7.33 +
    7.34 +    let result = [];
    7.35 +    for (let i = 0; i < uuid.length; i++) {
    7.36 +        if (i == 4 || i == 6 || i == 8 || i == 10) {
    7.37 +            result.push('-');
    7.38 +        }
    7.39 +        result.push(uuid[i].toString(16).padStart(2, '0'));
    7.40 +    }
    7.41 +
    7.42 +    return result.join('');
    7.43 +}
    7.44 +
    7.45 +function parseFirefoxVersion(firefoxVersionString) {
    7.46 +    let [, versionString, releaseTypeAbbrev = '', releaseNumberString = '0'] =
    7.47 +            FIREFOX_VERSION_RE.exec(firefoxVersionString);
    7.48 +
    7.49 +    let releaseType = FIREFOX_RELEASE_TYPES[releaseTypeAbbrev];
    7.50 +
    7.51 +    let releaseNumber = parseInt(releaseNumberString);
    7.52 +    let [major = 0, minor = 0, patch = 0] = versionString.split('.')
    7.53 +            .map(x => parseInt(x));
    7.54 +
    7.55 +    return {
    7.56 +        major,
    7.57 +        minor,
    7.58 +        patch,
    7.59 +        releaseType,
    7.60 +        releaseNumber,
    7.61 +    };
    7.62 +}
    7.63 +
    7.64 +class Tab {
    7.65 +    static deserialize(object) {
    7.66 +        return new Tab(object);
    7.67 +    }
    7.68 +
    7.69 +    constructor({url, title, uuid = generateUuidV4String(), favIconUrl = null,
    7.70 +            thumbnailUrl = null}) {
    7.71 +        this.uuid = uuid;
    7.72 +        this.url = url;
    7.73 +        this.title = title;
    7.74 +        this.favIconUrl = favIconUrl;
    7.75 +        this.thumbnailUrl = thumbnailUrl;
    7.76 +    }
    7.77 +
    7.78 +    serialize() {
    7.79 +        return Object.assign({}, this);
    7.80 +    }
    7.81 +}
    7.82 +
    7.83 +class TabCollection {
    7.84 +    static deserialize(object) {
    7.85 +        object.tabs = Array.from(object.tabs,
    7.86 +                ([, tab]) => [tab.uuid, Tab.deserialize(tab)]);
    7.87 +        return new TabCollection(object);
    7.88 +    }
    7.89 +
    7.90 +    constructor({tabs, uuid = generateUuidV4String(), date = new Date()}) {
    7.91 +        this.uuid = uuid;
    7.92 +        this.date = new Date(date);
    7.93 +        this.tabs = new Map();
    7.94 +        // allow any type which allows iteration
    7.95 +        for (let [, tab] of tabs) {
    7.96 +            this.tabs.set(tab.uuid, tab);
    7.97 +        }
    7.98 +    }
    7.99 +
   7.100 +    serialize() {
   7.101 +        let serializedTabs = [];
   7.102 +        for (let [tabUuid, tab] of this.tabs) {
   7.103 +            serializedTabs.push([tab.uuid, tab.serialize()]);
   7.104 +        }
   7.105 +        return {
   7.106 +            uuid: this.uuid,
   7.107 +            date: this.date.toJSON(),
   7.108 +            tabs: serializedTabs
   7.109 +        };
   7.110 +    }
   7.111 +}
   7.112 +
   7.113 +class TabCollectionsStorageProxy {
   7.114 +    constructor() {
   7.115 +        this.tabCollections = new Map();
   7.116 +        this.ports = new Set();
   7.117 +        this.browserVersion = undefined;
   7.118 +        this.messageQueue = [];
   7.119 +        this.isInitialized = false;
   7.120 +
   7.121 +        browser.runtime.onConnect.addListener(this.onConnect.bind(this));
   7.122 +    }
   7.123 +
   7.124 +    async init() {
   7.125 +        let browserInfo = await browser.runtime.getBrowserInfo();
   7.126 +        this.browserVersion = parseFirefoxVersion(browserInfo.version);
   7.127 +
   7.128 +        // get all tab collections and deserialize them in a Map
   7.129 +        let storageEntries = Object.entries(await browser.storage.sync.get())
   7.130 +                .filter(([key, value]) => GROUP_KEY_RE.test(key))
   7.131 +                .map(([key, tabCollection]) =>
   7.132 +                [tabCollection.uuid, TabCollection.deserialize(tabCollection)]);
   7.133 +        this.tabCollections = new Map(storageEntries);
   7.134 +        console.log('tab collections from storage');
   7.135 +        console.table(this.tabCollections);
   7.136 +        console.groupEnd();
   7.137 +        browser.storage.onChanged.addListener(this.onStorageChanged.bind(this));
   7.138 +
   7.139 +        this.isInitialized = true;
   7.140 +
   7.141 +        while (this.messageQueue.length > 0) {
   7.142 +            let [message, port] = this.messageQueue.pop();
   7.143 +            if (this.ports.has(port)) {
   7.144 +                this.onMessage(message, port);
   7.145 +            }
   7.146 +        }
   7.147 +    }
   7.148 +
   7.149 +    async createTabThumbnail(tabId) {
   7.150 +        let captureUrl = await browser.tabs.captureTab(tabId);
   7.151 +        let thumbnailUrl = await new Promise((resolve, reject) => {
   7.152 +            let image = new Image();
   7.153 +            image.addEventListener('load', ev => {
   7.154 +                let canvas = document.createElement('canvas');
   7.155 +                canvas.width = THUMBNAIL_WIDTH;
   7.156 +                canvas.height = THUMBNAIL_HEIGHT;
   7.157 +                let dWidth = canvas.width;
   7.158 +                let dHeight = dWidth * (image.height / image.width);
   7.159 +
   7.160 +                let ctx = canvas.getContext('2d');
   7.161 +                ctx.fillStyle = '#fff';
   7.162 +                ctx.fillRect(0, 0, canvas.width, canvas.height);
   7.163 +                ctx.drawImage(image, 0, 0, dWidth, dHeight);
   7.164 +
   7.165 +                resolve(canvas.toDataURL('image/jpeg', 0.75));
   7.166 +            });
   7.167 +            image.addEventListener('error', e => {
   7.168 +                reject(e);
   7.169 +            });
   7.170 +            image.src = captureUrl;
   7.171 +        });
   7.172 +        return thumbnailUrl;
   7.173 +    }
   7.174 +
   7.175 +    async createTabCollection(windowId) {
   7.176 +        let browserTabs = await browser.tabs.query({
   7.177 +            windowId,
   7.178 +            hidden: false,
   7.179 +            pinned:false
   7.180 +        });
   7.181 +
   7.182 +        // sanity check to prevent saving tabs from incognito windows
   7.183 +        if (browserTabs.length === 0 || browserTabs[0].incognito) {
   7.184 +            return;
   7.185 +        }
   7.186 +
   7.187 +        // filter out tabs which cannot be restored
   7.188 +        browserTabs = browserTabs.filter(browserTab =>
   7.189 +                SUPPORTED_PROTOCOLS.includes(new URL(browserTab.url).protocol));
   7.190 +        if (browserTabs.length === 0) {
   7.191 +            return;
   7.192 +        }
   7.193 +
   7.194 +        let tabs = browserTabs.map(browserTab => {
   7.195 +            let tab = new Tab({
   7.196 +                url: browserTab.url,
   7.197 +                title: browserTab.title,
   7.198 +                favIconUrl: browserTab.favIconUrl
   7.199 +            });
   7.200 +            return [tab.uuid, tab];
   7.201 +        });
   7.202 +
   7.203 +        // create empty tab which becomes the new active tab
   7.204 +        await browser.tabs.create({active: true});
   7.205 +
   7.206 +        // capture tabs, return null for discarded tabs since they can only be
   7.207 +        // captured after they have been restored, e.g. through user
   7.208 +        // interaction, and thus might hang the capture process indefinetly
   7.209 +        let thumbnails = await Promise.all(browserTabs.map(browserTab =>
   7.210 +                !browserTab.discarded ?
   7.211 +                this.createTabThumbnail(browserTab.id) : null));
   7.212 +        for (let [, tab] of tabs) {
   7.213 +            tab.thumbnailUrl = thumbnails.shift();
   7.214 +        }
   7.215 +
   7.216 +        let tabCollection = new TabCollection({tabs});
   7.217 +        console.log('created tab collection:', tabCollection);
   7.218 +
   7.219 +        // store tab collection
   7.220 +        console.log('storing tab collection:', tabCollection);
   7.221 +        await browser.storage.sync.set({
   7.222 +            [`collection:${tabCollection.uuid}`]: tabCollection.serialize()
   7.223 +        });
   7.224 +
   7.225 +        // remove tabs
   7.226 +        await browser.tabs.remove(browserTabs.map(browserTab => browserTab.id));
   7.227 +    }
   7.228 +
   7.229 +    async removeTab(tabCollectionUuid, tabUuid) {
   7.230 +        console.log('removing tab %s from collection %s', tabUuid,
   7.231 +                tabCollectionUuid);
   7.232 +        let tabCollection = this.tabCollections.get(tabCollectionUuid);
   7.233 +        // create shallow clone
   7.234 +        let newTabCollection = new TabCollection(tabCollection);
   7.235 +        newTabCollection.tabs.delete(tabUuid);
   7.236 +        // remove tab collection if there are no more tabs
   7.237 +        if (newTabCollection.tabs.size === 0) {
   7.238 +            return this.removeTabCollection(tabCollectionUuid);
   7.239 +        }
   7.240 +        await browser.storage.sync.set({
   7.241 +            [`collection:${tabCollectionUuid}`]: newTabCollection.serialize()
   7.242 +        });
   7.243 +    }
   7.244 +
   7.245 +    async restoreTab(tabCollectionUuid, tabUuid, windowId) {
   7.246 +        console.log('restoring tab %s from collection %s in window %d', tabUuid,
   7.247 +                tabCollectionUuid, windowId);
   7.248 +        let tab = this.tabCollections.get(tabCollectionUuid).tabs.get(tabUuid);
   7.249 +        let tabProperties = {
   7.250 +            active: false,
   7.251 +            url: tab.url,
   7.252 +            windowId
   7.253 +        };
   7.254 +        if (this.browserVersion.major >= 63) {
   7.255 +            tabProperties.discarded = true;
   7.256 +        }
   7.257 +        await browser.tabs.create(tabProperties);
   7.258 +        await this.removeTab(tabCollectionUuid, tabUuid);
   7.259 +    }
   7.260 +
   7.261 +    async removeTabCollection(tabCollectionUuid) {
   7.262 +        console.log('removing tab collection %s', tabCollectionUuid);
   7.263 +        await browser.storage.sync.remove(`collection:${tabCollectionUuid}`);
   7.264 +    }
   7.265 +
   7.266 +    async restoreTabCollection(tabCollectionUuid, windowId) {
   7.267 +        console.log('restoring tab collection %s in window %s',
   7.268 +                tabCollectionUuid, windowId);
   7.269 +        let tabProperties = {
   7.270 +            active: false,
   7.271 +            windowId
   7.272 +        };
   7.273 +        if (this.browserVersion.major >= 63) {
   7.274 +            tabProperties.discarded = true;
   7.275 +        }
   7.276 +        let creatingTabs =
   7.277 +                Array.from(this.tabCollections.get(tabCollectionUuid).tabs,
   7.278 +                ([, tab]) => browser.tabs.create(Object.assign({
   7.279 +                    url: tab.url
   7.280 +                }, tabProperties)));
   7.281 +        await Promise.all(creatingTabs);
   7.282 +        await this.removeTabCollection(tabCollectionUuid);
   7.283 +    }
   7.284 +
   7.285 +    onStorageChanged(changes, areaName) {
   7.286 +        if (areaName !== 'sync') {
   7.287 +            return;
   7.288 +        }
   7.289 +
   7.290 +        console.group('sync storage area changed:', changes);
   7.291 +        console.table(Object.entries(changes)[0][1])
   7.292 +        console.groupEnd();
   7.293 +
   7.294 +        let [key, {oldValue, newValue}] = Object.entries(changes)[0];
   7.295 +        if (!GROUP_KEY_RE.test(key)) {
   7.296 +            return;
   7.297 +        }
   7.298 +
   7.299 +        let tabCollectionUuid = key.replace('collection:', '');
   7.300 +        if (typeof oldValue === 'undefined') {
   7.301 +            // a new collection was created
   7.302 +            let newTabCollection = TabCollection.deserialize(newValue);
   7.303 +            this.tabCollections.set(tabCollectionUuid, newTabCollection);
   7.304 +
   7.305 +            this.broadcastMessage({
   7.306 +                type: 'tabCollectionCreated',
   7.307 +                tabCollection: newTabCollection
   7.308 +            });
   7.309 +        } else if (typeof newValue === 'undefined') {
   7.310 +            // a collection has been removed
   7.311 +            this.tabCollections.delete(tabCollectionUuid);
   7.312 +
   7.313 +            this.broadcastMessage({
   7.314 +                type: 'tabCollectionRemoved',
   7.315 +                tabCollectionUuid
   7.316 +            });
   7.317 +        } else {
   7.318 +            // a collection has changed
   7.319 +            let newTabCollection = TabCollection.deserialize(newValue);
   7.320 +            this.tabCollections.set(tabCollectionUuid, newTabCollection);
   7.321 +
   7.322 +            this.broadcastMessage({
   7.323 +                type: 'tabCollectionChanged',
   7.324 +                tabCollection: newTabCollection
   7.325 +            });
   7.326 +        }
   7.327 +    }
   7.328 +
   7.329 +    broadcastMessage(message) {
   7.330 +        for (let port of this.ports) {
   7.331 +            port.postMessage(message);
   7.332 +        }
   7.333 +    }
   7.334 +
   7.335 +    onConnect(port) {
   7.336 +        console.log('port connected:', port)
   7.337 +        this.ports.add(port);
   7.338 +        port.onMessage.addListener(this.onMessage.bind(this));
   7.339 +        port.onDisconnect.addListener(this.onDisconnect.bind(this));
   7.340 +    }
   7.341 +
   7.342 +    onDisconnect(port) {
   7.343 +        if (port.error) {
   7.344 +            console.log(`port connection error: ${port.error}\n`);
   7.345 +        }
   7.346 +        console.log('port disconnected:', port);
   7.347 +        this.ports.delete(port);
   7.348 +    }
   7.349 +
   7.350 +    onMessage(message, port) {
   7.351 +        if (!this.isInitialized) {
   7.352 +            console.log('queued message', message, 'from port', port);
   7.353 +            this.messageQueue.push([message, port]);
   7.354 +            return;
   7.355 +        }
   7.356 +
   7.357 +        console.log('received message', message, 'on port', port);
   7.358 +        switch (message.type) {
   7.359 +            case 'getTabCollections':
   7.360 +                port.postMessage({
   7.361 +                    type: 'tabCollections',
   7.362 +                    tabCollections: this.tabCollections
   7.363 +                });
   7.364 +                break;
   7.365 +            case 'removeTab':
   7.366 +                this.removeTab(message.tabCollectionUuid, message.tabUuid);
   7.367 +                break;
   7.368 +            case 'restoreTab':
   7.369 +                this.restoreTab(message.tabCollectionUuid, message.tabUuid,
   7.370 +                        message.windowId);
   7.371 +                break;
   7.372 +            case 'removeTabCollection':
   7.373 +                this.removeTabCollection(message.tabCollectionUuid);
   7.374 +                break;
   7.375 +            case 'restoreTabCollection':
   7.376 +                this.restoreTabCollection(message.tabCollectionUuid,
   7.377 +                        message.windowId);
   7.378 +                break;
   7.379 +        }
   7.380 +    }
   7.381 +}
   7.382 +
   7.383 +// browser action context menu entry for opening the sidebar
   7.384 +browser.menus.create({
   7.385 +    contexts: ['browser_action'],
   7.386 +    onclick: (info, tab) => browser.sidebarAction.open(),
   7.387 +    title: browser.i18n.getMessage('showTabsMenuItem')
   7.388 +});
   7.389 +
   7.390 +// disable the browser action for new incognito tabs
   7.391 +browser.tabs.onCreated.addListener(tab => {
   7.392 +    if (tab.incognito) {
   7.393 +        // this does not work, it seems that the browser action is re-enabled
   7.394 +        // on every update
   7.395 +        browser.browserAction.disable(tab.id);
   7.396 +    }
   7.397 +});
   7.398 +
   7.399 +(async () => {
   7.400 +    // disable the browser action for existing incognito tabs
   7.401 +    let tabs = await browser.tabs.query({});
   7.402 +    await Promise.all(tabs.filter(tab => tab.incognito)
   7.403 +            .map(tab => browser.browserAction.disable(tab.id)))
   7.404 +
   7.405 +    tabCollectionsProxy = new TabCollectionsStorageProxy();
   7.406 +    await tabCollectionsProxy.init();
   7.407 +
   7.408 +    browser.browserAction.onClicked.addListener(async targetTab => {
   7.409 +        // prevent browser action from being activated while a collection is
   7.410 +        // being created
   7.411 +        let tabs = await browser.tabs.query({windowId: targetTab.windowId});
   7.412 +        await Promise.all(tabs.map(tab =>
   7.413 +                browser.browserAction.disable(tab.id)));
   7.414 +
   7.415 +        try {
   7.416 +            await tabCollectionsProxy.createTabCollection(targetTab.windowId);
   7.417 +        } catch (e) {
   7.418 +            tabs = await browser.tabs.query({windowId: targetTab.windowId});
   7.419 +            await Promise.all(tabs.map(tab =>
   7.420 +                    browser.browserAction.enable(tab.id)));
   7.421 +            throw e
   7.422 +        }
   7.423 +
   7.424 +        tabs = await browser.tabs.query({windowId: targetTab.windowId});
   7.425 +        await Promise.all(tabs.map(tab =>
   7.426 +                browser.browserAction.enable(tab.id)));
   7.427 +    });
   7.428 +})();
     8.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     8.2 +++ b/icons/set-aside-action-dark.svg	Sat Nov 17 10:44:16 2018 +0100
     8.3 @@ -0,0 +1,14 @@
     8.4 +<!--
     8.5 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
     8.6 +
     8.7 +This Source Code Form is subject to the terms of the Mozilla Public
     8.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
     8.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
    8.10 +-->
    8.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
    8.12 +  <g fill="none" opacity=".8" stroke="#0c0c0d" stroke-width="1">
    8.13 +    <path d="m6 11v1c0 1.1 0.89 2 2 2h4c1.1 0 2-0.89 2-2v-8c0-1.1-0.89-2-2-2h-4c-1.1 0-2 0.89-2 2v4" stroke-width="2"/>
    8.14 +    <path d="m6 5.5h8"/>
    8.15 +    <path d="m1.5 9.5h9m-9 0 2 2m-2-2 2-2" stroke-linecap="round"/>
    8.16 +  </g>
    8.17 +</svg>
     9.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     9.2 +++ b/icons/set-aside-action-light.svg	Sat Nov 17 10:44:16 2018 +0100
     9.3 @@ -0,0 +1,14 @@
     9.4 +<!--
     9.5 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
     9.6 +
     9.7 +This Source Code Form is subject to the terms of the Mozilla Public
     9.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
     9.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
    9.10 +-->
    9.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
    9.12 +  <g fill="none" opacity=".8" stroke="#f9f9fa" stroke-width="1">
    9.13 +    <path d="m6 11v1c0 1.1 0.89 2 2 2h4c1.1 0 2-0.89 2-2v-8c0-1.1-0.89-2-2-2h-4c-1.1 0-2 0.89-2 2v4" stroke-width="2"/>
    9.14 +    <path d="m6 5.5h8"/>
    9.15 +    <path d="m1.5 9.5h9m-9 0 2 2m-2-2 2-2" stroke-linecap="round"/>
    9.16 +  </g>
    9.17 +</svg>
    10.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    10.2 +++ b/icons/set-aside-sidebar.svg	Sat Nov 17 10:44:16 2018 +0100
    10.3 @@ -0,0 +1,16 @@
    10.4 +<!--
    10.5 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    10.6 +
    10.7 +This Source Code Form is subject to the terms of the Mozilla Public
    10.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
    10.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
   10.10 +-->
   10.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
   10.12 +  <g fill="none" opacity=".8" stroke="#0c0c0d" stroke-width="1">
   10.13 +    <path d="m6 4c0-1.1 0.89-2 2-2h4c1.1 0 2 0.89 2 2v5c0 1.1-0.89 2-2 2h-1"
   10.14 +    stroke-width="2"/>
   10.15 +    <path d="m10 5.5h4"/>
   10.16 +    <rect x="2" y="5" width="8" height="9" rx="2" ry="2" stroke-width="2"/>
   10.17 +    <path d="m2 8.5h8"/>
   10.18 +  </g>
   10.19 +</svg>
    11.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    11.2 +++ b/icons/set-aside.svg	Sat Nov 17 10:44:16 2018 +0100
    11.3 @@ -0,0 +1,15 @@
    11.4 +<!--
    11.5 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    11.6 +
    11.7 +This Source Code Form is subject to the terms of the Mozilla Public
    11.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
    11.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
   11.10 +-->
   11.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
   11.12 +  <rect width="16" height="16" rx="3" ry="3" fill="#008ea4"/>
   11.13 +  <g fill="none" stroke="#ffffff" stroke-width="1">
   11.14 +    <path d="m6 11v1c0 1.1 0.89 2 2 2h4c1.1 0 2-0.89 2-2v-8c0-1.1-0.89-2-2-2h-4c-1.1 0-2 0.89-2 2v4" stroke-width="2"/>
   11.15 +    <path d="m6 5.5h8"/>
   11.16 +    <path d="m1.5 9.5h9m-9 0 2 2m-2-2 2-2" stroke-linecap="round"/>
   11.17 +  </g>
   11.18 +</svg>
    12.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    12.2 +++ b/manifest.json.in	Sat Nov 17 10:44:16 2018 +0100
    12.3 @@ -0,0 +1,52 @@
    12.4 +{
    12.5 +    "manifest_version": 2,
    12.6 +    "name": "__MSG_extensionName__",
    12.7 +    "version": "@VERSION@",
    12.8 +    "description": "__MSG_extensionDescription__",
    12.9 +    "author": "Guido Berhoerster",
   12.10 +    "homepage_url": "https://code.guido-berhoerster.org/addons/firefox-addons/set-aside/",
   12.11 +    "default_locale": "en",
   12.12 +    "applications": {
   12.13 +        "gecko": {
   12.14 +            "id": "set-aside@code.guido-berhoerster.org",
   12.15 +            "strict_min_version": "60.0"
   12.16 +        }
   12.17 +    },
   12.18 +    "icons": {
   12.19 +        "48": "icons/set-aside-48.png",
   12.20 +        "96": "icons/set-aside-96.png"
   12.21 +    },
   12.22 +    "permissions": [
   12.23 +        "tabs",
   12.24 +        "storage",
   12.25 +        "sessions",
   12.26 +        "menus",
   12.27 +        "<all_urls>"
   12.28 +    ],
   12.29 +    "background": {
   12.30 +        "scripts": [ "background.js" ]
   12.31 +    },
   12.32 +    "browser_action": {
   12.33 +        "default_area": "tabstrip",
   12.34 +        "default_title": "__MSG_defaultBrowserActionTitle__",
   12.35 +        "theme_icons": [{
   12.36 +            "dark": "icons/set-aside-action-dark-16.png",
   12.37 +            "light": "icons/set-aside-action-light-16.png",
   12.38 +            "size": 16
   12.39 +        }, {
   12.40 +            "dark": "icons/set-aside-action-dark-32.png",
   12.41 +            "light": "icons/set-aside-action-light-32.png",
   12.42 +            "size": 32
   12.43 +        }]
   12.44 +    },
   12.45 +    "sidebar_action": {
   12.46 +        "browser_style": true,
   12.47 +        "default_icon": {
   12.48 +            "16": "icons/set-aside-sidebar-16.png",
   12.49 +            "32": "icons/set-aside-sidebar-32.png"
   12.50 +        },
   12.51 +        "default_panel": "sidebar/tab-collection-manager.html",
   12.52 +        "default_title": "__MSG_defaultSidebarTitle__",
   12.53 +        "open_at_install": false
   12.54 +    }
   12.55 +}
    13.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    13.2 +++ b/sidebar/images/defaultFavicon.svg	Sat Nov 17 10:44:16 2018 +0100
    13.3 @@ -0,0 +1,8 @@
    13.4 +<!-- This icon was created by the Mozilla Project and taken from
    13.5 +     Firefox 65.0a1. -->
    13.6 +<!-- This Source Code Form is subject to the terms of the Mozilla Public
    13.7 +   - License, v. 2.0. If a copy of the MPL was not distributed with this
    13.8 +   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
    13.9 +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
   13.10 +  <path fill="context-fill" fill-opacity="context-fill-opacity" d="M8 0a8 8 0 1 0 8 8 8.009 8.009 0 0 0-8-8zm5.163 4.958h-1.552a7.7 7.7 0 0 0-1.051-2.376 6.03 6.03 0 0 1 2.603 2.376zM14 8a5.963 5.963 0 0 1-.335 1.958h-1.821A12.327 12.327 0 0 0 12 8a12.327 12.327 0 0 0-.156-1.958h1.821A5.963 5.963 0 0 1 14 8zm-6 6c-1.075 0-2.037-1.2-2.567-2.958h5.135C10.037 12.8 9.075 14 8 14zM5.174 9.958a11.084 11.084 0 0 1 0-3.916h5.651A11.114 11.114 0 0 1 11 8a11.114 11.114 0 0 1-.174 1.958zM2 8a5.963 5.963 0 0 1 .335-1.958h1.821a12.361 12.361 0 0 0 0 3.916H2.335A5.963 5.963 0 0 1 2 8zm6-6c1.075 0 2.037 1.2 2.567 2.958H5.433C5.963 3.2 6.925 2 8 2zm-2.56.582a7.7 7.7 0 0 0-1.051 2.376H2.837A6.03 6.03 0 0 1 5.44 2.582zm-2.6 8.46h1.549a7.7 7.7 0 0 0 1.051 2.376 6.03 6.03 0 0 1-2.603-2.376zm7.723 2.376a7.7 7.7 0 0 0 1.051-2.376h1.552a6.03 6.03 0 0 1-2.606 2.376z"/>
   13.11 +</svg>
    14.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    14.2 +++ b/sidebar/images/remove.svg	Sat Nov 17 10:44:16 2018 +0100
    14.3 @@ -0,0 +1,11 @@
    14.4 +<!--
    14.5 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    14.6 +
    14.7 +This Source Code Form is subject to the terms of the Mozilla Public
    14.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
    14.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
   14.10 +-->
   14.11 +<svg version="1.1" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
   14.12 +  <path d="m4.5 4.5 7 7m0 -7-7 7" stroke="#0c0c0d" stroke-opacity="1"
   14.13 +  stroke-linecap="round" stroke-width="1.5"/>
   14.14 +</svg>
    15.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    15.2 +++ b/sidebar/images/thumbnail-placeholder.svg	Sat Nov 17 10:44:16 2018 +0100
    15.3 @@ -0,0 +1,16 @@
    15.4 +<!--
    15.5 +Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    15.6 +
    15.7 +This Source Code Form is subject to the terms of the Mozilla Public
    15.8 +License, v. 2.0. If a copy of the MPL was not distributed with this
    15.9 +file, You can obtain one at http://mozilla.org/MPL/2.0/.
   15.10 +-->
   15.11 +<svg version="1.1" viewBox="0 0 224 128" xmlns="http://www.w3.org/2000/svg">
   15.12 +  <g fill="#ededf0">
   15.13 +    <rect x="8" y="8" width="208" height="32" rx="4" ry="4"/>
   15.14 +    <rect x="8" y="48" width="40" height="72" rx="4" ry="4"/>
   15.15 +    <rect x="56" y="48" width="160" height="24" rx="4" ry="4"/>
   15.16 +    <rect x="56" y="80" width="160" height="16" rx="4" ry="4"/>
   15.17 +    <rect x="56" y="104" width="160" height="16" rx="4" ry="4"/>
   15.18 +  </g>
   15.19 +</svg>
    16.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    16.2 +++ b/sidebar/js/tab-collection-manager.js	Sat Nov 17 10:44:16 2018 +0100
    16.3 @@ -0,0 +1,222 @@
    16.4 +/*
    16.5 + * Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    16.6 + *
    16.7 + * This Source Code Form is subject to the terms of the Mozilla Public
    16.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
    16.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
   16.10 + */
   16.11 +
   16.12 +'use strict';
   16.13 +
   16.14 +var tabManager;
   16.15 +
   16.16 +class TabManager {
   16.17 +    constructor() {
   16.18 +        this.tabCollectionTemplate =
   16.19 +                document.querySelector('#tab-collection-template');
   16.20 +        this.tabItemTemplate = document.querySelector('#tab-item-template');
   16.21 +        this.tabCollectionsElement = document.querySelector('#tab-collections');
   16.22 +        this.port = browser.runtime.connect({name: 'tab-collection-manager'});
   16.23 +        this.port.onMessage.addListener(this.onMessage.bind(this));
   16.24 +        this.port.onDisconnect.addListener(this.onMessage.bind(this));
   16.25 +        this.port.postMessage({type: 'getTabCollections'});
   16.26 +        this.isInitialized = false;
   16.27 +    }
   16.28 +
   16.29 +    initTabCollections(tabCollections) {
   16.30 +        if (this.isInitialized) {
   16.31 +            return;
   16.32 +        }
   16.33 +
   16.34 +        for (let tabCollection of tabCollections.values()) {
   16.35 +            this.prependTabCollection(tabCollection);
   16.36 +        }
   16.37 +        this.sortTabCollections();
   16.38 +
   16.39 +        document.querySelector('#message').textContent =
   16.40 +                browser.i18n.getMessage('emptySidebarMessage');
   16.41 +
   16.42 +        document.body.addEventListener('click', this);
   16.43 +
   16.44 +        this.isInitialized = true;
   16.45 +    }
   16.46 +
   16.47 +    createTabCollectionNode(tabCollection) {
   16.48 +        let tabCollectionNode =
   16.49 +                document.importNode(this.tabCollectionTemplate.content, true);
   16.50 +
   16.51 +        tabCollectionNode.querySelector('.tab-collection')
   16.52 +                .dataset.tabCollectionUuid = tabCollection.uuid;
   16.53 +
   16.54 +        tabCollectionNode.querySelector('.tab-collection-title').textContent =
   16.55 +                browser.i18n.getMessage('collectionTitle',
   16.56 +                tabCollection.tabs.size);
   16.57 +
   16.58 +        let tabCollectionCtimeElement =
   16.59 +                tabCollectionNode.querySelector('.tab-collection-ctime');
   16.60 +        tabCollectionCtimeElement.dateTime = tabCollection.date.toISOString();
   16.61 +        tabCollectionCtimeElement.textContent =
   16.62 +                tabCollection.date.toLocaleString();
   16.63 +
   16.64 +        let tabCollectionRestoreElement =
   16.65 +                tabCollectionNode.querySelector('.restore-tab-collection');
   16.66 +        tabCollectionRestoreElement.title =
   16.67 +                tabCollectionRestoreElement.textContent =
   16.68 +                browser.i18n.getMessage('restoreTabsButtonTitle');
   16.69 +        tabCollectionNode.querySelector('.remove-tab-collection').title =
   16.70 +                browser.i18n.getMessage('removeTabsButtonTitle');
   16.71 +
   16.72 +        let tabListElement =
   16.73 +                tabCollectionNode.querySelector('.tab-collection-tabs');
   16.74 +        for (let tab of tabCollection.tabs.values()) {
   16.75 +            let tabItemNode = document.importNode(this.tabItemTemplate.content,
   16.76 +                    true);
   16.77 +
   16.78 +            tabItemNode.querySelector('.tab-item').dataset.tabUuid = tab.uuid;
   16.79 +
   16.80 +            let tabLinkElement = tabItemNode.querySelector('.tab-link');
   16.81 +            tabLinkElement.href = tab.url;
   16.82 +            tabLinkElement.title = tab.title;
   16.83 +
   16.84 +            if (tab.thumbnailUrl !== null) {
   16.85 +                tabItemNode.querySelector('.tab-thumbnail').src =
   16.86 +                        tab.thumbnailUrl;
   16.87 +            }
   16.88 +
   16.89 +            if (tab.favIconUrl !== null) {
   16.90 +                tabItemNode.querySelector('.tab-favicon').src = tab.favIconUrl;
   16.91 +            }
   16.92 +
   16.93 +            tabItemNode.querySelector('.tab-title').textContent = tab.title;
   16.94 +
   16.95 +            tabItemNode.querySelector('.remove-tab').title =
   16.96 +                    browser.i18n.getMessage('removeTabTitle');
   16.97 +
   16.98 +            tabListElement.append(tabItemNode);
   16.99 +        }
  16.100 +
  16.101 +        return tabCollectionNode;
  16.102 +    }
  16.103 +
  16.104 +    prependTabCollection(tabCollection) {
  16.105 +        console.log('prepending tab collection', tabCollection,
  16.106 +                'to tab collections');
  16.107 +        this.tabCollectionsElement
  16.108 +                .prepend(this.createTabCollectionNode(tabCollection));
  16.109 +    }
  16.110 +
  16.111 +    replaceTabCollection(tabCollection) {
  16.112 +        console.log('replacing tab collection', tabCollection);
  16.113 +        this.tabCollectionsElement.querySelector(`[data-tab-collection-uuid=` +
  16.114 +                `"${tabCollection.uuid}"]`)
  16.115 +                .replaceWith(this.createTabCollectionNode(tabCollection));
  16.116 +    }
  16.117 +
  16.118 +    removeTabCollection(tabCollectionUuid) {
  16.119 +        console.log('removing tab collection %s', tabCollectionUuid);
  16.120 +        this.tabCollectionsElement
  16.121 +                .querySelector(`[data-tab-collection-uuid=` +
  16.122 +                `"${tabCollectionUuid}"]`)
  16.123 +                .remove();
  16.124 +
  16.125 +        if (this.tabCollectionsElement.childElementCount === 0) {
  16.126 +            // remove any text nodes so that the :empty CSS selectora applies
  16.127 +            while (this.tabCollectionsElement.firstChild !== null) {
  16.128 +                this.tabCollectionsElement
  16.129 +                        .removeChild(this.tabCollectionsElement.firstChild);
  16.130 +            }
  16.131 +        }
  16.132 +    }
  16.133 +
  16.134 +    sortTabCollections() {
  16.135 +        Array.from(this.tabCollectionsElement.children)
  16.136 +                .map(element =>
  16.137 +                [element.querySelector('.tab-collection-ctime').dateTime,
  16.138 +                element])
  16.139 +                .sort((a, b) => a[0] < b[0] ? 1 : a[0] > b[0] ? -1 : 0)
  16.140 +                .forEach(([, element]) =>
  16.141 +                this.tabCollectionsElement.append(element));
  16.142 +    }
  16.143 +
  16.144 +    onMessage(message, port) {
  16.145 +        console.log('received message', message, 'on port', port);
  16.146 +        switch (message.type) {
  16.147 +            case 'tabCollections':
  16.148 +                this.initTabCollections(message.tabCollections);
  16.149 +                break;
  16.150 +            case 'tabCollectionCreated':
  16.151 +                this.prependTabCollection(message.tabCollection);
  16.152 +                break;
  16.153 +            case 'tabCollectionRemoved':
  16.154 +                this.removeTabCollection(message.tabCollectionUuid);
  16.155 +                break;
  16.156 +            case 'tabCollectionChanged':
  16.157 +                this.replaceTabCollection(message.tabCollection);
  16.158 +                break;
  16.159 +        }
  16.160 +        this.sortTabCollections();
  16.161 +    }
  16.162 +
  16.163 +    handleEvent(ev) {
  16.164 +        console.log('DOM event', ev);
  16.165 +        if (ev.type === 'click') {
  16.166 +            ev.preventDefault();
  16.167 +            if (ev.target.classList.contains('restore-tab-collection')) {
  16.168 +                // restore tab collection
  16.169 +                let tabCollectionUuid = ev.target.closest('.tab-collection')
  16.170 +                        .dataset.tabCollectionUuid;
  16.171 +                this.port.postMessage({
  16.172 +                    type: 'restoreTabCollection',
  16.173 +                    tabCollectionUuid,
  16.174 +                    windowId: browser.windows.WINDOW_ID_CURRENT
  16.175 +                });
  16.176 +            } else if (ev.target.classList.contains('remove-tab-collection')) {
  16.177 +                // remove tab collection
  16.178 +                let tabCollectionUuid = ev.target.closest('.tab-collection')
  16.179 +                        .dataset.tabCollectionUuid;
  16.180 +                this.port.postMessage({
  16.181 +                    type: 'removeTabCollection',
  16.182 +                    tabCollectionUuid
  16.183 +                });
  16.184 +            } else if (ev.target.classList.contains('remove-tab')) {
  16.185 +                // remove tab from collection
  16.186 +                let tabItemElement = ev.target.closest('.tab-item');
  16.187 +                let tabCollectionUuid =
  16.188 +                        tabItemElement.closest('.tab-collection')
  16.189 +                        .dataset.tabCollectionUuid;
  16.190 +                let tabUuid = tabItemElement.dataset.tabUuid;
  16.191 +                this.port.postMessage({
  16.192 +                    type: 'removeTab',
  16.193 +                    tabCollectionUuid,
  16.194 +                    tabUuid
  16.195 +                });
  16.196 +            } else {
  16.197 +                let tabItemElement = ev.target.closest('.tab-item');
  16.198 +                if (tabItemElement !== null) {
  16.199 +                    // restore tab from collection
  16.200 +                    let tabCollectionUuid =
  16.201 +                            tabItemElement.closest('.tab-collection')
  16.202 +                            .dataset.tabCollectionUuid;
  16.203 +                    let tabUuid = tabItemElement.dataset.tabUuid;
  16.204 +                    this.port.postMessage({
  16.205 +                        type: 'restoreTab',
  16.206 +                        tabCollectionUuid,
  16.207 +                        tabUuid,
  16.208 +                        windowId: browser.windows.WINDOW_ID_CURRENT
  16.209 +                    });
  16.210 +                }
  16.211 +            }
  16.212 +        }
  16.213 +    }
  16.214 +}
  16.215 +
  16.216 +browser.windows.getCurrent().then(currentWindow => {
  16.217 +    // disable the sidebar for incognito windows
  16.218 +    if (currentWindow.incognito) {
  16.219 +        document.querySelector('#message').textContent =
  16.220 +                browser.i18n.getMessage('incognitoModeMessage');
  16.221 +        return;
  16.222 +    }
  16.223 +
  16.224 +    tabManager = new TabManager();
  16.225 +});
    17.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    17.2 +++ b/sidebar/style/photon-colors.css	Sat Nov 17 10:44:16 2018 +0100
    17.3 @@ -0,0 +1,91 @@
    17.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
    17.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
    17.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
    17.7 +
    17.8 +/* Photon Colors CSS Variables v3.2.0 */
    17.9 +
   17.10 +:root {
   17.11 +  --magenta-50: #ff1ad9;
   17.12 +  --magenta-60: #ed00b5;
   17.13 +  --magenta-70: #b5007f;
   17.14 +  --magenta-80: #7d004f;
   17.15 +  --magenta-90: #440027;
   17.16 +
   17.17 +  --purple-30: #c069ff;
   17.18 +  --purple-40: #ad3bff;
   17.19 +  --purple-50: #9400ff;
   17.20 +  --purple-60: #8000d7;
   17.21 +  --purple-70: #6200a4;
   17.22 +  --purple-80: #440071;
   17.23 +  --purple-90: #25003e;
   17.24 +
   17.25 +  --blue-40: #45a1ff;
   17.26 +  --blue-50: #0a84ff;
   17.27 +  --blue-50-a30: rgba(10, 132, 255, 0.3);
   17.28 +  --blue-60: #0060df;
   17.29 +  --blue-70: #003eaa;
   17.30 +  --blue-80: #002275;
   17.31 +  --blue-90: #000f40;
   17.32 +
   17.33 +  --teal-50: #00feff;
   17.34 +  --teal-60: #00c8d7;
   17.35 +  --teal-70: #008ea4;
   17.36 +  --teal-80: #005a71;
   17.37 +  --teal-90: #002d3e;
   17.38 +
   17.39 +  --green-50: #30e60b;
   17.40 +  --green-60: #12bc00;
   17.41 +  --green-70: #058b00;
   17.42 +  --green-80: #006504;
   17.43 +  --green-90: #003706;
   17.44 +
   17.45 +  --yellow-50: #ffe900;
   17.46 +  --yellow-60: #d7b600;
   17.47 +  --yellow-70: #a47f00;
   17.48 +  --yellow-80: #715100;
   17.49 +  --yellow-90: #3e2800;
   17.50 +
   17.51 +  --red-50: #ff0039;
   17.52 +  --red-60: #d70022;
   17.53 +  --red-70: #a4000f;
   17.54 +  --red-80: #5a0002;
   17.55 +  --red-90: #3e0200;
   17.56 +
   17.57 +  --orange-50: #ff9400;
   17.58 +  --orange-60: #d76e00;
   17.59 +  --orange-70: #a44900;
   17.60 +  --orange-80: #712b00;
   17.61 +  --orange-90: #3e1300;
   17.62 +
   17.63 +  --grey-10: #f9f9fa;
   17.64 +  --grey-10-a10: rgba(249, 249, 250, 0.1);
   17.65 +  --grey-10-a20: rgba(249, 249, 250, 0.2);
   17.66 +  --grey-10-a40: rgba(249, 249, 250, 0.4);
   17.67 +  --grey-10-a60: rgba(249, 249, 250, 0.6);
   17.68 +  --grey-10-a80: rgba(249, 249, 250, 0.8);
   17.69 +  --grey-20: #ededf0;
   17.70 +  --grey-30: #d7d7db;
   17.71 +  --grey-40: #b1b1b3;
   17.72 +  --grey-50: #737373;
   17.73 +  --grey-60: #4a4a4f;
   17.74 +  --grey-70: #38383d;
   17.75 +  --grey-80: #2a2a2e;
   17.76 +  --grey-90: #0c0c0d;
   17.77 +  --grey-90-a05: rgba(12, 12, 13, 0.05);
   17.78 +  --grey-90-a10: rgba(12, 12, 13, 0.1);
   17.79 +  --grey-90-a20: rgba(12, 12, 13, 0.2);
   17.80 +  --grey-90-a30: rgba(12, 12, 13, 0.3);
   17.81 +  --grey-90-a40: rgba(12, 12, 13, 0.4);
   17.82 +  --grey-90-a50: rgba(12, 12, 13, 0.5);
   17.83 +  --grey-90-a60: rgba(12, 12, 13, 0.6);
   17.84 +  --grey-90-a70: rgba(12, 12, 13, 0.7);
   17.85 +  --grey-90-a80: rgba(12, 12, 13, 0.8);
   17.86 +  --grey-90-a90: rgba(12, 12, 13, 0.9);
   17.87 +
   17.88 +  --ink-70: #363959;
   17.89 +  --ink-80: #202340;
   17.90 +  --ink-90: #0f1126;
   17.91 +
   17.92 +  --white-100: #ffffff;
   17.93 +
   17.94 +}
    18.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    18.2 +++ b/sidebar/style/tab-collection-manager.css	Sat Nov 17 10:44:16 2018 +0100
    18.3 @@ -0,0 +1,209 @@
    18.4 +/*
    18.5 + * Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
    18.6 + *
    18.7 + * This Source Code Form is subject to the terms of the Mozilla Public
    18.8 + * License, v. 2.0. If a copy of the MPL was not distributed with this
    18.9 + * file, You can obtain one at http://mozilla.org/MPL/2.0/.
   18.10 + */
   18.11 +
   18.12 +@import url("chrome://browser/content/extension.css");
   18.13 +@import url("photon-colors.css");
   18.14 +
   18.15 +:root {
   18.16 +  --box-shadow-border: inset 0 0 0 1px var(--grey-90-a10);
   18.17 +  --button-highlight-color: var(--grey-90-a10);
   18.18 +  --secondary-text-color: var(--grey-50);
   18.19 +  --font-size-display-20: 36px;
   18.20 +  --font-size-title-20: 17px;
   18.21 +  --font-size-body-20: 15px;
   18.22 +  --font-size-body-10: 13px;
   18.23 +
   18.24 +  --thumbnail-width: 224px;
   18.25 +  --thumbnail-height: 128px;
   18.26 +  --thumbnail-shadow: var(--box-shadow-border), 0 1px 4px var(--grey-90-a30);
   18.27 +  --thumbnail-shadow-highlight: var(--thumbnail-shadow),
   18.28 +      0 0 0 5px var(--grey-30);
   18.29 +
   18.30 +  --remove-tab-collection-image-width: 16px;
   18.31 +  --remove-tab-collection-image-height:
   18.32 +      var(--remove-tab-collection-image-width);
   18.33 +  --remove-tab-collection-padding: 2px;
   18.34 +  --remove-tab-collection-width: calc(2 * var(--remove-tab-collection-padding) +
   18.35 +      var(--remove-tab-collection-image-width));
   18.36 +  --remove-tab-collection-height: var(--remove-tab-collection-width);
   18.37 +
   18.38 +  --favicon-image-width: 32px;
   18.39 +  --favicon-padding: 4px;
   18.40 +  --favicon-width: calc(2 * var(--favicon-padding) +
   18.41 +      var(--favicon-image-width));
   18.42 +  --favicon-height: var(--favicon-width);
   18.43 +  --favicon-overlap: 6px;
   18.44 +
   18.45 +  --remove-tab-image-width: 16px;
   18.46 +  --remove-tab-image-height: var(--remove-tab-image-width);
   18.47 +  --remove-tab-padding: 4px;
   18.48 +  --remove-tab-width: calc(var(--remove-tab-padding) * 2 +
   18.49 +      var(--remove-tab-image-width));
   18.50 +  --remove-tab-height: var(--remove-tab-width);
   18.51 +  --remove-tab-overlap: calc(var(--remove-tab-width) / 2);
   18.52 +}
   18.53 +
   18.54 +:any-link {
   18.55 +  text-decoration: none;
   18.56 +  color: inherit;
   18.57 +}
   18.58 +
   18.59 +body {
   18.60 +  font-size: var(--font-size-body-20);
   18.61 +  font-weight: 400;
   18.62 +}
   18.63 +
   18.64 +figure {
   18.65 +  margin: 0;
   18.66 +}
   18.67 +
   18.68 +#message {
   18.69 +  margin-top: 1em;
   18.70 +  color: var(--secondary-text-color);
   18.71 +  font-size: var(--font-size-display-20);
   18.72 +  font-weight: 300;
   18.73 +  text-align: center;
   18.74 +}
   18.75 +
   18.76 +#tab-collections:not(:empty) ~ #message {
   18.77 +  display: none;
   18.78 +}
   18.79 +
   18.80 +#tab-collections {
   18.81 +  display: flex;
   18.82 +  flex-direction: column;
   18.83 +}
   18.84 +
   18.85 +.tab-collection {
   18.86 +  margin: 16px 0;
   18.87 +}
   18.88 +
   18.89 +.tab-collection-header {
   18.90 +  display: flex;
   18.91 +  flex-wrap: wrap;
   18.92 +  align-items: baseline;
   18.93 +  margin: 4px;
   18.94 +}
   18.95 +
   18.96 +.tab-collection-title {
   18.97 +  font-size: var(--font-size-title-20);
   18.98 +  font-weight: 500;
   18.99 +}
  18.100 +
  18.101 +.tab-collection-title,
  18.102 +.tab-collection-ctime {
  18.103 +  white-space: nowrap;
  18.104 +  margin: 0 0 0 6px;
  18.105 +}
  18.106 +
  18.107 +.tab-collection-ctime {
  18.108 +  font-size: var(--font-size-body-10);
  18.109 +  color: var(--secondary-text-color);
  18.110 +}
  18.111 +
  18.112 +.tab-collection-actions,
  18.113 +.tab-collection-tabs {
  18.114 +  display: flex;
  18.115 +  padding-left: 0;
  18.116 +  margin: 0;
  18.117 +  list-style: none;
  18.118 +  overflow: auto;
  18.119 +}
  18.120 +
  18.121 +.tab-collection-actions {
  18.122 +  font-size: var(--font-size-body-10);
  18.123 +  justify-content: flex-end;
  18.124 +  white-space: nowrap;
  18.125 +  margin: 4px 8px;
  18.126 +  align-items: center;
  18.127 +}
  18.128 +
  18.129 +.tab-collection-actions > li + li {
  18.130 +  margin-left: 8px;
  18.131 +}
  18.132 +
  18.133 +.remove-tab-collection {
  18.134 +  width: var(--remove-tab-collection-width);
  18.135 +  height: var(--remove-tab-collection-height);
  18.136 +  background: url(../images/remove.svg) var(--remove-tab-collection-padding)/var(--remove-tab-collection-image-width) no-repeat;
  18.137 +  border-radius: 2px;
  18.138 +}
  18.139 +
  18.140 +.remove-tab-collection:hover {
  18.141 +  background-color: var(--grey-90-a10);
  18.142 +}
  18.143 +
  18.144 +.restore-tab-collection {
  18.145 +  cursor: pointer;
  18.146 +}
  18.147 +
  18.148 +.restore-tab-collection:hover {
  18.149 +  text-decoration: underline;
  18.150 +}
  18.151 +
  18.152 +.tab-collection-tabs {
  18.153 +  margin: 4px 0 0 0;
  18.154 +}
  18.155 +
  18.156 +.tab-item {
  18.157 +  display: block;
  18.158 +  position: relative;
  18.159 +  width: var(--thumbnail-width);
  18.160 +  margin: var(--remove-tab-overlap) var(--remove-tab-overlap) 4px
  18.161 +      var(--remove-tab-overlap);
  18.162 +}
  18.163 +
  18.164 +.tab-link:any-link:hover .tab-thumbnail,
  18.165 +.tab-link:any-link:focus .tab-thumbnail,
  18.166 +.tab-link:any-link:active .tab-thumbnail {
  18.167 +  box-shadow: var(--thumbnail-shadow-highlight);
  18.168 +}
  18.169 +
  18.170 +.tab-thumbnail {
  18.171 +  display: block;
  18.172 +  margin: 0;
  18.173 +  border-radius: 6px;
  18.174 +  box-shadow: var(--thumbnail-shadow);
  18.175 +  transition: box-shadow 150ms;
  18.176 +}
  18.177 +
  18.178 +.tab-title {
  18.179 +  font-size: var(--font-size-body-10);
  18.180 +  white-space: nowrap;
  18.181 +  overflow: hidden;
  18.182 +  text-overflow: ellipsis;
  18.183 +  min-width: 0;
  18.184 +  margin: calc(var(--favicon-overlap) + 2px) 0 0 0;
  18.185 +}
  18.186 +
  18.187 +.tab-favicon {
  18.188 +  position: absolute;
  18.189 +  top: calc(var(--thumbnail-height) - var(--favicon-height) +
  18.190 +       var(--favicon-overlap));
  18.191 +  right: calc(-1 * var(--favicon-overlap));
  18.192 +  width: var(--favicon-width);
  18.193 +  height: var(--favicon-height);
  18.194 +  padding: var(--favicon-padding);
  18.195 +  background-color: var(--white-100);
  18.196 +  border-radius: 6px;
  18.197 +  box-shadow: inset 0 0 0 1px var(--grey-90-a10);
  18.198 +  cursor: pointer;
  18.199 +}
  18.200 +
  18.201 +.remove-tab {
  18.202 +  position: absolute;
  18.203 +  top: calc(-1 * var(--remove-tab-overlap));
  18.204 +  right: calc(-1 * var(--remove-tab-overlap));
  18.205 +  width: var(--remove-tab-width);
  18.206 +  height: var(--remove-tab-height);
  18.207 +  background: url(../images/remove.svg)
  18.208 +      var(--remove-tab-padding)/var(--remove-tab-image-width) no-repeat
  18.209 +      var(--white-100);
  18.210 +  border-radius: 100%;
  18.211 +  box-shadow: var(--box-shadow-border);
  18.212 +}
    19.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
    19.2 +++ b/sidebar/tab-collection-manager.html	Sat Nov 17 10:44:16 2018 +0100
    19.3 @@ -0,0 +1,49 @@
    19.4 +<!doctype html>
    19.5 +<html>
    19.6 +  <head>
    19.7 +    <meta charset="utf-8">
    19.8 +<!--
    19.9 +   Copyright (C) 2018 Guido Berhoerster <guido+set-aside@berhoerster.name>
   19.10 +
   19.11 +   This Source Code Form is subject to the terms of the Mozilla Public
   19.12 +   License, v. 2.0. If a copy of the MPL was not distributed with this
   19.13 +   file, You can obtain one at http://mozilla.org/MPL/2.0/.
   19.14 +-->
   19.15 +    <script src="js/tab-collection-manager.js" defer></script>
   19.16 +    <link rel="stylesheet" href="style/tab-collection-manager.css">
   19.17 +  </head>
   19.18 +  <body>
   19.19 +    <template id="tab-collection-template">
   19.20 +      <div class="tab-collection" data-tab-collection-uuid="">
   19.21 +        <div class="tab-collection-header">
   19.22 +          <h2 class="tab-collection-title"></h2>
   19.23 +          <time class="tab-collection-ctime" datetime=""></time>
   19.24 +        </div>
   19.25 +        <ul class="tab-collection-actions">
   19.26 +          <li><div class="restore-tab-collection" title=""></div></li>
   19.27 +          <li><div class="remove-tab-collection" title=""></div></li>
   19.28 +        </ul>
   19.29 +        <ul class="tab-collection-tabs">
   19.30 +        </ul>
   19.31 +      </div>
   19.32 +    </template>
   19.33 +    <template id="tab-item-template">
   19.34 +      <li class="tab-item" data-tab-uuid="">
   19.35 +        <a class="tab-link" href="" title="">
   19.36 +          <figure>
   19.37 +            <img class="tab-thumbnail" width="224" height="128"
   19.38 +            src="images/thumbnail-placeholder.svg" alt="">
   19.39 +            <figcaption class="tab-title"></figcaption>
   19.40 +          </figure>
   19.41 +          <img class="tab-favicon" width="32" height="32"
   19.42 +          src="images/defaultFavicon.svg" alt="">
   19.43 +        </a>
   19.44 +        <div class="remove-tab" href="" title=""></div>
   19.45 +      </li>
   19.46 +    </template>
   19.47 +    <div id="tab-collections"><!-- leave empty so that the :empty CSS selector
   19.48 +    applies --></div>
   19.49 +    <div id="message">
   19.50 +    </div>
   19.51 +  </body>
   19.52 +</html>