├── .gitignore ├── LICENSE ├── README.md ├── assets ├── fonts │ └── material │ │ ├── MaterialIcons-Regular.eot │ │ ├── MaterialIcons-Regular.ijmap │ │ ├── MaterialIcons-Regular.svg │ │ ├── MaterialIcons-Regular.ttf │ │ ├── MaterialIcons-Regular.woff │ │ ├── MaterialIcons-Regular.woff2 │ │ └── codepoints ├── images │ ├── icon-dark.svg │ ├── icon-light.svg │ ├── logo.png │ └── logo.svg └── styles │ ├── checkmarks-commons.css │ └── checkmarks-material.css ├── commons └── checkmarks-commons.js ├── manifest.json ├── options ├── checkmarks-options.css ├── checkmarks-options.html └── checkmarks-options.js ├── run.sh └── sidebar ├── checkmarks-sidebar.css ├── checkmarks-sidebar.html └── checkmarks-sidebar.js /.gitignore: -------------------------------------------------------------------------------- 1 | web-ext-artifacts 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CHECKMARKS 2 | 3 | https://addons.mozilla.org/en-US/firefox/addon/checkmarks-web-ext/ -------------------------------------------------------------------------------- /assets/fonts/material/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanwald/checkmarks/8ea20e2fdb756bf0d6a790eb0110b24732548329/assets/fonts/material/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /assets/fonts/material/MaterialIcons-Regular.ijmap: -------------------------------------------------------------------------------- 1 | {"icons":{"e84d":{"name":"3d Rotation"},"eb3b":{"name":"Ac Unit"},"e190":{"name":"Access Alarm"},"e191":{"name":"Access Alarms"},"e192":{"name":"Access Time"},"e84e":{"name":"Accessibility"},"e914":{"name":"Accessible"},"e84f":{"name":"Account Balance"},"e850":{"name":"Account Balance Wallet"},"e851":{"name":"Account Box"},"e853":{"name":"Account Circle"},"e60e":{"name":"Adb"},"e145":{"name":"Add"},"e439":{"name":"Add A Photo"},"e193":{"name":"Add Alarm"},"e003":{"name":"Add Alert"},"e146":{"name":"Add Box"},"e147":{"name":"Add Circle"},"e148":{"name":"Add Circle Outline"},"e567":{"name":"Add Location"},"e854":{"name":"Add Shopping Cart"},"e39d":{"name":"Add To Photos"},"e05c":{"name":"Add To Queue"},"e39e":{"name":"Adjust"},"e630":{"name":"Airline Seat Flat"},"e631":{"name":"Airline Seat Flat Angled"},"e632":{"name":"Airline Seat Individual Suite"},"e633":{"name":"Airline Seat Legroom Extra"},"e634":{"name":"Airline Seat Legroom Normal"},"e635":{"name":"Airline Seat Legroom Reduced"},"e636":{"name":"Airline Seat Recline Extra"},"e637":{"name":"Airline Seat Recline Normal"},"e195":{"name":"Airplanemode Active"},"e194":{"name":"Airplanemode Inactive"},"e055":{"name":"Airplay"},"eb3c":{"name":"Airport Shuttle"},"e855":{"name":"Alarm"},"e856":{"name":"Alarm Add"},"e857":{"name":"Alarm Off"},"e858":{"name":"Alarm On"},"e019":{"name":"Album"},"eb3d":{"name":"All Inclusive"},"e90b":{"name":"All Out"},"e859":{"name":"Android"},"e85a":{"name":"Announcement"},"e5c3":{"name":"Apps"},"e149":{"name":"Archive"},"e5c4":{"name":"Arrow Back"},"e5db":{"name":"Arrow Downward"},"e5c5":{"name":"Arrow Drop Down"},"e5c6":{"name":"Arrow Drop Down Circle"},"e5c7":{"name":"Arrow Drop Up"},"e5c8":{"name":"Arrow Forward"},"e5d8":{"name":"Arrow Upward"},"e060":{"name":"Art Track"},"e85b":{"name":"Aspect Ratio"},"e85c":{"name":"Assessment"},"e85d":{"name":"Assignment"},"e85e":{"name":"Assignment Ind"},"e85f":{"name":"Assignment Late"},"e860":{"name":"Assignment Return"},"e861":{"name":"Assignment Returned"},"e862":{"name":"Assignment Turned In"},"e39f":{"name":"Assistant"},"e3a0":{"name":"Assistant Photo"},"e226":{"name":"Attach File"},"e227":{"name":"Attach Money"},"e2bc":{"name":"Attachment"},"e3a1":{"name":"Audiotrack"},"e863":{"name":"Autorenew"},"e01b":{"name":"Av Timer"},"e14a":{"name":"Backspace"},"e864":{"name":"Backup"},"e19c":{"name":"Battery Alert"},"e1a3":{"name":"Battery Charging Full"},"e1a4":{"name":"Battery Full"},"e1a5":{"name":"Battery Std"},"e1a6":{"name":"Battery Unknown"},"eb3e":{"name":"Beach Access"},"e52d":{"name":"Beenhere"},"e14b":{"name":"Block"},"e1a7":{"name":"Bluetooth"},"e60f":{"name":"Bluetooth Audio"},"e1a8":{"name":"Bluetooth Connected"},"e1a9":{"name":"Bluetooth Disabled"},"e1aa":{"name":"Bluetooth Searching"},"e3a2":{"name":"Blur Circular"},"e3a3":{"name":"Blur Linear"},"e3a4":{"name":"Blur Off"},"e3a5":{"name":"Blur On"},"e865":{"name":"Book"},"e866":{"name":"Bookmark"},"e867":{"name":"Bookmark Border"},"e228":{"name":"Border All"},"e229":{"name":"Border Bottom"},"e22a":{"name":"Border Clear"},"e22b":{"name":"Border Color"},"e22c":{"name":"Border Horizontal"},"e22d":{"name":"Border Inner"},"e22e":{"name":"Border Left"},"e22f":{"name":"Border Outer"},"e230":{"name":"Border Right"},"e231":{"name":"Border Style"},"e232":{"name":"Border Top"},"e233":{"name":"Border Vertical"},"e06b":{"name":"Branding Watermark"},"e3a6":{"name":"Brightness 1"},"e3a7":{"name":"Brightness 2"},"e3a8":{"name":"Brightness 3"},"e3a9":{"name":"Brightness 4"},"e3aa":{"name":"Brightness 5"},"e3ab":{"name":"Brightness 6"},"e3ac":{"name":"Brightness 7"},"e1ab":{"name":"Brightness Auto"},"e1ac":{"name":"Brightness High"},"e1ad":{"name":"Brightness Low"},"e1ae":{"name":"Brightness Medium"},"e3ad":{"name":"Broken Image"},"e3ae":{"name":"Brush"},"e6dd":{"name":"Bubble Chart"},"e868":{"name":"Bug Report"},"e869":{"name":"Build"},"e43c":{"name":"Burst Mode"},"e0af":{"name":"Business"},"eb3f":{"name":"Business Center"},"e86a":{"name":"Cached"},"e7e9":{"name":"Cake"},"e0b0":{"name":"Call"},"e0b1":{"name":"Call End"},"e0b2":{"name":"Call Made"},"e0b3":{"name":"Call Merge"},"e0b4":{"name":"Call Missed"},"e0e4":{"name":"Call Missed Outgoing"},"e0b5":{"name":"Call Received"},"e0b6":{"name":"Call Split"},"e06c":{"name":"Call To Action"},"e3af":{"name":"Camera"},"e3b0":{"name":"Camera Alt"},"e8fc":{"name":"Camera Enhance"},"e3b1":{"name":"Camera Front"},"e3b2":{"name":"Camera Rear"},"e3b3":{"name":"Camera Roll"},"e5c9":{"name":"Cancel"},"e8f6":{"name":"Card Giftcard"},"e8f7":{"name":"Card Membership"},"e8f8":{"name":"Card Travel"},"eb40":{"name":"Casino"},"e307":{"name":"Cast"},"e308":{"name":"Cast Connected"},"e3b4":{"name":"Center Focus Strong"},"e3b5":{"name":"Center Focus Weak"},"e86b":{"name":"Change History"},"e0b7":{"name":"Chat"},"e0ca":{"name":"Chat Bubble"},"e0cb":{"name":"Chat Bubble Outline"},"e5ca":{"name":"Check"},"e834":{"name":"Check Box"},"e835":{"name":"Check Box Outline Blank"},"e86c":{"name":"Check Circle"},"e5cb":{"name":"Chevron Left"},"e5cc":{"name":"Chevron Right"},"eb41":{"name":"Child Care"},"eb42":{"name":"Child Friendly"},"e86d":{"name":"Chrome Reader Mode"},"e86e":{"name":"Class"},"e14c":{"name":"Clear"},"e0b8":{"name":"Clear All"},"e5cd":{"name":"Close"},"e01c":{"name":"Closed Caption"},"e2bd":{"name":"Cloud"},"e2be":{"name":"Cloud Circle"},"e2bf":{"name":"Cloud Done"},"e2c0":{"name":"Cloud Download"},"e2c1":{"name":"Cloud Off"},"e2c2":{"name":"Cloud Queue"},"e2c3":{"name":"Cloud Upload"},"e86f":{"name":"Code"},"e3b6":{"name":"Collections"},"e431":{"name":"Collections Bookmark"},"e3b7":{"name":"Color Lens"},"e3b8":{"name":"Colorize"},"e0b9":{"name":"Comment"},"e3b9":{"name":"Compare"},"e915":{"name":"Compare Arrows"},"e30a":{"name":"Computer"},"e638":{"name":"Confirmation Number"},"e0d0":{"name":"Contact Mail"},"e0cf":{"name":"Contact Phone"},"e0ba":{"name":"Contacts"},"e14d":{"name":"Content Copy"},"e14e":{"name":"Content Cut"},"e14f":{"name":"Content Paste"},"e3ba":{"name":"Control Point"},"e3bb":{"name":"Control Point Duplicate"},"e90c":{"name":"Copyright"},"e150":{"name":"Create"},"e2cc":{"name":"Create New Folder"},"e870":{"name":"Credit Card"},"e3be":{"name":"Crop"},"e3bc":{"name":"Crop 16 9"},"e3bd":{"name":"Crop 3 2"},"e3bf":{"name":"Crop 5 4"},"e3c0":{"name":"Crop 7 5"},"e3c1":{"name":"Crop Din"},"e3c2":{"name":"Crop Free"},"e3c3":{"name":"Crop Landscape"},"e3c4":{"name":"Crop Original"},"e3c5":{"name":"Crop Portrait"},"e437":{"name":"Crop Rotate"},"e3c6":{"name":"Crop Square"},"e871":{"name":"Dashboard"},"e1af":{"name":"Data Usage"},"e916":{"name":"Date Range"},"e3c7":{"name":"Dehaze"},"e872":{"name":"Delete"},"e92b":{"name":"Delete Forever"},"e16c":{"name":"Delete Sweep"},"e873":{"name":"Description"},"e30b":{"name":"Desktop Mac"},"e30c":{"name":"Desktop Windows"},"e3c8":{"name":"Details"},"e30d":{"name":"Developer Board"},"e1b0":{"name":"Developer Mode"},"e335":{"name":"Device Hub"},"e1b1":{"name":"Devices"},"e337":{"name":"Devices Other"},"e0bb":{"name":"Dialer Sip"},"e0bc":{"name":"Dialpad"},"e52e":{"name":"Directions"},"e52f":{"name":"Directions Bike"},"e532":{"name":"Directions Boat"},"e530":{"name":"Directions Bus"},"e531":{"name":"Directions Car"},"e534":{"name":"Directions Railway"},"e566":{"name":"Directions Run"},"e533":{"name":"Directions Subway"},"e535":{"name":"Directions Transit"},"e536":{"name":"Directions Walk"},"e610":{"name":"Disc Full"},"e875":{"name":"Dns"},"e612":{"name":"Do Not Disturb"},"e611":{"name":"Do Not Disturb Alt"},"e643":{"name":"Do Not Disturb Off"},"e644":{"name":"Do Not Disturb On"},"e30e":{"name":"Dock"},"e7ee":{"name":"Domain"},"e876":{"name":"Done"},"e877":{"name":"Done All"},"e917":{"name":"Donut Large"},"e918":{"name":"Donut Small"},"e151":{"name":"Drafts"},"e25d":{"name":"Drag Handle"},"e613":{"name":"Drive Eta"},"e1b2":{"name":"Dvr"},"e3c9":{"name":"Edit"},"e568":{"name":"Edit Location"},"e8fb":{"name":"Eject"},"e0be":{"name":"Email"},"e63f":{"name":"Enhanced Encryption"},"e01d":{"name":"Equalizer"},"e000":{"name":"Error"},"e001":{"name":"Error Outline"},"e926":{"name":"Euro Symbol"},"e56d":{"name":"Ev Station"},"e878":{"name":"Event"},"e614":{"name":"Event Available"},"e615":{"name":"Event Busy"},"e616":{"name":"Event Note"},"e903":{"name":"Event Seat"},"e879":{"name":"Exit To App"},"e5ce":{"name":"Expand Less"},"e5cf":{"name":"Expand More"},"e01e":{"name":"Explicit"},"e87a":{"name":"Explore"},"e3ca":{"name":"Exposure"},"e3cb":{"name":"Exposure Neg 1"},"e3cc":{"name":"Exposure Neg 2"},"e3cd":{"name":"Exposure Plus 1"},"e3ce":{"name":"Exposure Plus 2"},"e3cf":{"name":"Exposure Zero"},"e87b":{"name":"Extension"},"e87c":{"name":"Face"},"e01f":{"name":"Fast Forward"},"e020":{"name":"Fast Rewind"},"e87d":{"name":"Favorite"},"e87e":{"name":"Favorite Border"},"e06d":{"name":"Featured Play List"},"e06e":{"name":"Featured Video"},"e87f":{"name":"Feedback"},"e05d":{"name":"Fiber Dvr"},"e061":{"name":"Fiber Manual Record"},"e05e":{"name":"Fiber New"},"e06a":{"name":"Fiber Pin"},"e062":{"name":"Fiber Smart Record"},"e2c4":{"name":"File Download"},"e2c6":{"name":"File Upload"},"e3d3":{"name":"Filter"},"e3d0":{"name":"Filter 1"},"e3d1":{"name":"Filter 2"},"e3d2":{"name":"Filter 3"},"e3d4":{"name":"Filter 4"},"e3d5":{"name":"Filter 5"},"e3d6":{"name":"Filter 6"},"e3d7":{"name":"Filter 7"},"e3d8":{"name":"Filter 8"},"e3d9":{"name":"Filter 9"},"e3da":{"name":"Filter 9 Plus"},"e3db":{"name":"Filter B And W"},"e3dc":{"name":"Filter Center Focus"},"e3dd":{"name":"Filter Drama"},"e3de":{"name":"Filter Frames"},"e3df":{"name":"Filter Hdr"},"e152":{"name":"Filter List"},"e3e0":{"name":"Filter None"},"e3e2":{"name":"Filter Tilt Shift"},"e3e3":{"name":"Filter Vintage"},"e880":{"name":"Find In Page"},"e881":{"name":"Find Replace"},"e90d":{"name":"Fingerprint"},"e5dc":{"name":"First Page"},"eb43":{"name":"Fitness Center"},"e153":{"name":"Flag"},"e3e4":{"name":"Flare"},"e3e5":{"name":"Flash Auto"},"e3e6":{"name":"Flash Off"},"e3e7":{"name":"Flash On"},"e539":{"name":"Flight"},"e904":{"name":"Flight Land"},"e905":{"name":"Flight Takeoff"},"e3e8":{"name":"Flip"},"e882":{"name":"Flip To Back"},"e883":{"name":"Flip To Front"},"e2c7":{"name":"Folder"},"e2c8":{"name":"Folder Open"},"e2c9":{"name":"Folder Shared"},"e617":{"name":"Folder Special"},"e167":{"name":"Font Download"},"e234":{"name":"Format Align Center"},"e235":{"name":"Format Align Justify"},"e236":{"name":"Format Align Left"},"e237":{"name":"Format Align Right"},"e238":{"name":"Format Bold"},"e239":{"name":"Format Clear"},"e23a":{"name":"Format Color Fill"},"e23b":{"name":"Format Color Reset"},"e23c":{"name":"Format Color Text"},"e23d":{"name":"Format Indent Decrease"},"e23e":{"name":"Format Indent Increase"},"e23f":{"name":"Format Italic"},"e240":{"name":"Format Line Spacing"},"e241":{"name":"Format List Bulleted"},"e242":{"name":"Format List Numbered"},"e243":{"name":"Format Paint"},"e244":{"name":"Format Quote"},"e25e":{"name":"Format Shapes"},"e245":{"name":"Format Size"},"e246":{"name":"Format Strikethrough"},"e247":{"name":"Format Textdirection L To R"},"e248":{"name":"Format Textdirection R To L"},"e249":{"name":"Format Underlined"},"e0bf":{"name":"Forum"},"e154":{"name":"Forward"},"e056":{"name":"Forward 10"},"e057":{"name":"Forward 30"},"e058":{"name":"Forward 5"},"eb44":{"name":"Free Breakfast"},"e5d0":{"name":"Fullscreen"},"e5d1":{"name":"Fullscreen Exit"},"e24a":{"name":"Functions"},"e927":{"name":"G Translate"},"e30f":{"name":"Gamepad"},"e021":{"name":"Games"},"e90e":{"name":"Gavel"},"e155":{"name":"Gesture"},"e884":{"name":"Get App"},"e908":{"name":"Gif"},"eb45":{"name":"Golf Course"},"e1b3":{"name":"Gps Fixed"},"e1b4":{"name":"Gps Not Fixed"},"e1b5":{"name":"Gps Off"},"e885":{"name":"Grade"},"e3e9":{"name":"Gradient"},"e3ea":{"name":"Grain"},"e1b8":{"name":"Graphic Eq"},"e3eb":{"name":"Grid Off"},"e3ec":{"name":"Grid On"},"e7ef":{"name":"Group"},"e7f0":{"name":"Group Add"},"e886":{"name":"Group Work"},"e052":{"name":"Hd"},"e3ed":{"name":"Hdr Off"},"e3ee":{"name":"Hdr On"},"e3f1":{"name":"Hdr Strong"},"e3f2":{"name":"Hdr Weak"},"e310":{"name":"Headset"},"e311":{"name":"Headset Mic"},"e3f3":{"name":"Healing"},"e023":{"name":"Hearing"},"e887":{"name":"Help"},"e8fd":{"name":"Help Outline"},"e024":{"name":"High Quality"},"e25f":{"name":"Highlight"},"e888":{"name":"Highlight Off"},"e889":{"name":"History"},"e88a":{"name":"Home"},"eb46":{"name":"Hot Tub"},"e53a":{"name":"Hotel"},"e88b":{"name":"Hourglass Empty"},"e88c":{"name":"Hourglass Full"},"e902":{"name":"Http"},"e88d":{"name":"Https"},"e3f4":{"name":"Image"},"e3f5":{"name":"Image Aspect Ratio"},"e0e0":{"name":"Import Contacts"},"e0c3":{"name":"Import Export"},"e912":{"name":"Important Devices"},"e156":{"name":"Inbox"},"e909":{"name":"Indeterminate Check Box"},"e88e":{"name":"Info"},"e88f":{"name":"Info Outline"},"e890":{"name":"Input"},"e24b":{"name":"Insert Chart"},"e24c":{"name":"Insert Comment"},"e24d":{"name":"Insert Drive File"},"e24e":{"name":"Insert Emoticon"},"e24f":{"name":"Insert Invitation"},"e250":{"name":"Insert Link"},"e251":{"name":"Insert Photo"},"e891":{"name":"Invert Colors"},"e0c4":{"name":"Invert Colors Off"},"e3f6":{"name":"Iso"},"e312":{"name":"Keyboard"},"e313":{"name":"Keyboard Arrow Down"},"e314":{"name":"Keyboard Arrow Left"},"e315":{"name":"Keyboard Arrow Right"},"e316":{"name":"Keyboard Arrow Up"},"e317":{"name":"Keyboard Backspace"},"e318":{"name":"Keyboard Capslock"},"e31a":{"name":"Keyboard Hide"},"e31b":{"name":"Keyboard Return"},"e31c":{"name":"Keyboard Tab"},"e31d":{"name":"Keyboard Voice"},"eb47":{"name":"Kitchen"},"e892":{"name":"Label"},"e893":{"name":"Label Outline"},"e3f7":{"name":"Landscape"},"e894":{"name":"Language"},"e31e":{"name":"Laptop"},"e31f":{"name":"Laptop Chromebook"},"e320":{"name":"Laptop Mac"},"e321":{"name":"Laptop Windows"},"e5dd":{"name":"Last Page"},"e895":{"name":"Launch"},"e53b":{"name":"Layers"},"e53c":{"name":"Layers Clear"},"e3f8":{"name":"Leak Add"},"e3f9":{"name":"Leak Remove"},"e3fa":{"name":"Lens"},"e02e":{"name":"Library Add"},"e02f":{"name":"Library Books"},"e030":{"name":"Library Music"},"e90f":{"name":"Lightbulb Outline"},"e919":{"name":"Line Style"},"e91a":{"name":"Line Weight"},"e260":{"name":"Linear Scale"},"e157":{"name":"Link"},"e438":{"name":"Linked Camera"},"e896":{"name":"List"},"e0c6":{"name":"Live Help"},"e639":{"name":"Live Tv"},"e53f":{"name":"Local Activity"},"e53d":{"name":"Local Airport"},"e53e":{"name":"Local Atm"},"e540":{"name":"Local Bar"},"e541":{"name":"Local Cafe"},"e542":{"name":"Local Car Wash"},"e543":{"name":"Local Convenience Store"},"e556":{"name":"Local Dining"},"e544":{"name":"Local Drink"},"e545":{"name":"Local Florist"},"e546":{"name":"Local Gas Station"},"e547":{"name":"Local Grocery Store"},"e548":{"name":"Local Hospital"},"e549":{"name":"Local Hotel"},"e54a":{"name":"Local Laundry Service"},"e54b":{"name":"Local Library"},"e54c":{"name":"Local Mall"},"e54d":{"name":"Local Movies"},"e54e":{"name":"Local Offer"},"e54f":{"name":"Local Parking"},"e550":{"name":"Local Pharmacy"},"e551":{"name":"Local Phone"},"e552":{"name":"Local Pizza"},"e553":{"name":"Local Play"},"e554":{"name":"Local Post Office"},"e555":{"name":"Local Printshop"},"e557":{"name":"Local See"},"e558":{"name":"Local Shipping"},"e559":{"name":"Local Taxi"},"e7f1":{"name":"Location City"},"e1b6":{"name":"Location Disabled"},"e0c7":{"name":"Location Off"},"e0c8":{"name":"Location On"},"e1b7":{"name":"Location Searching"},"e897":{"name":"Lock"},"e898":{"name":"Lock Open"},"e899":{"name":"Lock Outline"},"e3fc":{"name":"Looks"},"e3fb":{"name":"Looks 3"},"e3fd":{"name":"Looks 4"},"e3fe":{"name":"Looks 5"},"e3ff":{"name":"Looks 6"},"e400":{"name":"Looks One"},"e401":{"name":"Looks Two"},"e028":{"name":"Loop"},"e402":{"name":"Loupe"},"e16d":{"name":"Low Priority"},"e89a":{"name":"Loyalty"},"e158":{"name":"Mail"},"e0e1":{"name":"Mail Outline"},"e55b":{"name":"Map"},"e159":{"name":"Markunread"},"e89b":{"name":"Markunread Mailbox"},"e322":{"name":"Memory"},"e5d2":{"name":"Menu"},"e252":{"name":"Merge Type"},"e0c9":{"name":"Message"},"e029":{"name":"Mic"},"e02a":{"name":"Mic None"},"e02b":{"name":"Mic Off"},"e618":{"name":"Mms"},"e253":{"name":"Mode Comment"},"e254":{"name":"Mode Edit"},"e263":{"name":"Monetization On"},"e25c":{"name":"Money Off"},"e403":{"name":"Monochrome Photos"},"e7f2":{"name":"Mood"},"e7f3":{"name":"Mood Bad"},"e619":{"name":"More"},"e5d3":{"name":"More Horiz"},"e5d4":{"name":"More Vert"},"e91b":{"name":"Motorcycle"},"e323":{"name":"Mouse"},"e168":{"name":"Move To Inbox"},"e02c":{"name":"Movie"},"e404":{"name":"Movie Creation"},"e43a":{"name":"Movie Filter"},"e6df":{"name":"Multiline Chart"},"e405":{"name":"Music Note"},"e063":{"name":"Music Video"},"e55c":{"name":"My Location"},"e406":{"name":"Nature"},"e407":{"name":"Nature People"},"e408":{"name":"Navigate Before"},"e409":{"name":"Navigate Next"},"e55d":{"name":"Navigation"},"e569":{"name":"Near Me"},"e1b9":{"name":"Network Cell"},"e640":{"name":"Network Check"},"e61a":{"name":"Network Locked"},"e1ba":{"name":"Network Wifi"},"e031":{"name":"New Releases"},"e16a":{"name":"Next Week"},"e1bb":{"name":"Nfc"},"e641":{"name":"No Encryption"},"e0cc":{"name":"No Sim"},"e033":{"name":"Not Interested"},"e06f":{"name":"Note"},"e89c":{"name":"Note Add"},"e7f4":{"name":"Notifications"},"e7f7":{"name":"Notifications Active"},"e7f5":{"name":"Notifications None"},"e7f6":{"name":"Notifications Off"},"e7f8":{"name":"Notifications Paused"},"e90a":{"name":"Offline Pin"},"e63a":{"name":"Ondemand Video"},"e91c":{"name":"Opacity"},"e89d":{"name":"Open In Browser"},"e89e":{"name":"Open In New"},"e89f":{"name":"Open With"},"e7f9":{"name":"Pages"},"e8a0":{"name":"Pageview"},"e40a":{"name":"Palette"},"e925":{"name":"Pan Tool"},"e40b":{"name":"Panorama"},"e40c":{"name":"Panorama Fish Eye"},"e40d":{"name":"Panorama Horizontal"},"e40e":{"name":"Panorama Vertical"},"e40f":{"name":"Panorama Wide Angle"},"e7fa":{"name":"Party Mode"},"e034":{"name":"Pause"},"e035":{"name":"Pause Circle Filled"},"e036":{"name":"Pause Circle Outline"},"e8a1":{"name":"Payment"},"e7fb":{"name":"People"},"e7fc":{"name":"People Outline"},"e8a2":{"name":"Perm Camera Mic"},"e8a3":{"name":"Perm Contact Calendar"},"e8a4":{"name":"Perm Data Setting"},"e8a5":{"name":"Perm Device Information"},"e8a6":{"name":"Perm Identity"},"e8a7":{"name":"Perm Media"},"e8a8":{"name":"Perm Phone Msg"},"e8a9":{"name":"Perm Scan Wifi"},"e7fd":{"name":"Person"},"e7fe":{"name":"Person Add"},"e7ff":{"name":"Person Outline"},"e55a":{"name":"Person Pin"},"e56a":{"name":"Person Pin Circle"},"e63b":{"name":"Personal Video"},"e91d":{"name":"Pets"},"e0cd":{"name":"Phone"},"e324":{"name":"Phone Android"},"e61b":{"name":"Phone Bluetooth Speaker"},"e61c":{"name":"Phone Forwarded"},"e61d":{"name":"Phone In Talk"},"e325":{"name":"Phone Iphone"},"e61e":{"name":"Phone Locked"},"e61f":{"name":"Phone Missed"},"e620":{"name":"Phone Paused"},"e326":{"name":"Phonelink"},"e0db":{"name":"Phonelink Erase"},"e0dc":{"name":"Phonelink Lock"},"e327":{"name":"Phonelink Off"},"e0dd":{"name":"Phonelink Ring"},"e0de":{"name":"Phonelink Setup"},"e410":{"name":"Photo"},"e411":{"name":"Photo Album"},"e412":{"name":"Photo Camera"},"e43b":{"name":"Photo Filter"},"e413":{"name":"Photo Library"},"e432":{"name":"Photo Size Select Actual"},"e433":{"name":"Photo Size Select Large"},"e434":{"name":"Photo Size Select Small"},"e415":{"name":"Picture As Pdf"},"e8aa":{"name":"Picture In Picture"},"e911":{"name":"Picture In Picture Alt"},"e6c4":{"name":"Pie Chart"},"e6c5":{"name":"Pie Chart Outlined"},"e55e":{"name":"Pin Drop"},"e55f":{"name":"Place"},"e037":{"name":"Play Arrow"},"e038":{"name":"Play Circle Filled"},"e039":{"name":"Play Circle Outline"},"e906":{"name":"Play For Work"},"e03b":{"name":"Playlist Add"},"e065":{"name":"Playlist Add Check"},"e05f":{"name":"Playlist Play"},"e800":{"name":"Plus One"},"e801":{"name":"Poll"},"e8ab":{"name":"Polymer"},"eb48":{"name":"Pool"},"e0ce":{"name":"Portable Wifi Off"},"e416":{"name":"Portrait"},"e63c":{"name":"Power"},"e336":{"name":"Power Input"},"e8ac":{"name":"Power Settings New"},"e91e":{"name":"Pregnant Woman"},"e0df":{"name":"Present To All"},"e8ad":{"name":"Print"},"e645":{"name":"Priority High"},"e80b":{"name":"Public"},"e255":{"name":"Publish"},"e8ae":{"name":"Query Builder"},"e8af":{"name":"Question Answer"},"e03c":{"name":"Queue"},"e03d":{"name":"Queue Music"},"e066":{"name":"Queue Play Next"},"e03e":{"name":"Radio"},"e837":{"name":"Radio Button Checked"},"e836":{"name":"Radio Button Unchecked"},"e560":{"name":"Rate Review"},"e8b0":{"name":"Receipt"},"e03f":{"name":"Recent Actors"},"e91f":{"name":"Record Voice Over"},"e8b1":{"name":"Redeem"},"e15a":{"name":"Redo"},"e5d5":{"name":"Refresh"},"e15b":{"name":"Remove"},"e15c":{"name":"Remove Circle"},"e15d":{"name":"Remove Circle Outline"},"e067":{"name":"Remove From Queue"},"e417":{"name":"Remove Red Eye"},"e928":{"name":"Remove Shopping Cart"},"e8fe":{"name":"Reorder"},"e040":{"name":"Repeat"},"e041":{"name":"Repeat One"},"e042":{"name":"Replay"},"e059":{"name":"Replay 10"},"e05a":{"name":"Replay 30"},"e05b":{"name":"Replay 5"},"e15e":{"name":"Reply"},"e15f":{"name":"Reply All"},"e160":{"name":"Report"},"e8b2":{"name":"Report Problem"},"e56c":{"name":"Restaurant"},"e561":{"name":"Restaurant Menu"},"e8b3":{"name":"Restore"},"e929":{"name":"Restore Page"},"e0d1":{"name":"Ring Volume"},"e8b4":{"name":"Room"},"eb49":{"name":"Room Service"},"e418":{"name":"Rotate 90 Degrees Ccw"},"e419":{"name":"Rotate Left"},"e41a":{"name":"Rotate Right"},"e920":{"name":"Rounded Corner"},"e328":{"name":"Router"},"e921":{"name":"Rowing"},"e0e5":{"name":"Rss Feed"},"e642":{"name":"Rv Hookup"},"e562":{"name":"Satellite"},"e161":{"name":"Save"},"e329":{"name":"Scanner"},"e8b5":{"name":"Schedule"},"e80c":{"name":"School"},"e1be":{"name":"Screen Lock Landscape"},"e1bf":{"name":"Screen Lock Portrait"},"e1c0":{"name":"Screen Lock Rotation"},"e1c1":{"name":"Screen Rotation"},"e0e2":{"name":"Screen Share"},"e623":{"name":"Sd Card"},"e1c2":{"name":"Sd Storage"},"e8b6":{"name":"Search"},"e32a":{"name":"Security"},"e162":{"name":"Select All"},"e163":{"name":"Send"},"e811":{"name":"Sentiment Dissatisfied"},"e812":{"name":"Sentiment Neutral"},"e813":{"name":"Sentiment Satisfied"},"e814":{"name":"Sentiment Very Dissatisfied"},"e815":{"name":"Sentiment Very Satisfied"},"e8b8":{"name":"Settings"},"e8b9":{"name":"Settings Applications"},"e8ba":{"name":"Settings Backup Restore"},"e8bb":{"name":"Settings Bluetooth"},"e8bd":{"name":"Settings Brightness"},"e8bc":{"name":"Settings Cell"},"e8be":{"name":"Settings Ethernet"},"e8bf":{"name":"Settings Input Antenna"},"e8c0":{"name":"Settings Input Component"},"e8c1":{"name":"Settings Input Composite"},"e8c2":{"name":"Settings Input Hdmi"},"e8c3":{"name":"Settings Input Svideo"},"e8c4":{"name":"Settings Overscan"},"e8c5":{"name":"Settings Phone"},"e8c6":{"name":"Settings Power"},"e8c7":{"name":"Settings Remote"},"e1c3":{"name":"Settings System Daydream"},"e8c8":{"name":"Settings Voice"},"e80d":{"name":"Share"},"e8c9":{"name":"Shop"},"e8ca":{"name":"Shop Two"},"e8cb":{"name":"Shopping Basket"},"e8cc":{"name":"Shopping Cart"},"e261":{"name":"Short Text"},"e6e1":{"name":"Show Chart"},"e043":{"name":"Shuffle"},"e1c8":{"name":"Signal Cellular 4 Bar"},"e1cd":{"name":"Signal Cellular Connected No Internet 4 Bar"},"e1ce":{"name":"Signal Cellular No Sim"},"e1cf":{"name":"Signal Cellular Null"},"e1d0":{"name":"Signal Cellular Off"},"e1d8":{"name":"Signal Wifi 4 Bar"},"e1d9":{"name":"Signal Wifi 4 Bar Lock"},"e1da":{"name":"Signal Wifi Off"},"e32b":{"name":"Sim Card"},"e624":{"name":"Sim Card Alert"},"e044":{"name":"Skip Next"},"e045":{"name":"Skip Previous"},"e41b":{"name":"Slideshow"},"e068":{"name":"Slow Motion Video"},"e32c":{"name":"Smartphone"},"eb4a":{"name":"Smoke Free"},"eb4b":{"name":"Smoking Rooms"},"e625":{"name":"Sms"},"e626":{"name":"Sms Failed"},"e046":{"name":"Snooze"},"e164":{"name":"Sort"},"e053":{"name":"Sort By Alpha"},"eb4c":{"name":"Spa"},"e256":{"name":"Space Bar"},"e32d":{"name":"Speaker"},"e32e":{"name":"Speaker Group"},"e8cd":{"name":"Speaker Notes"},"e92a":{"name":"Speaker Notes Off"},"e0d2":{"name":"Speaker Phone"},"e8ce":{"name":"Spellcheck"},"e838":{"name":"Star"},"e83a":{"name":"Star Border"},"e839":{"name":"Star Half"},"e8d0":{"name":"Stars"},"e0d3":{"name":"Stay Current Landscape"},"e0d4":{"name":"Stay Current Portrait"},"e0d5":{"name":"Stay Primary Landscape"},"e0d6":{"name":"Stay Primary Portrait"},"e047":{"name":"Stop"},"e0e3":{"name":"Stop Screen Share"},"e1db":{"name":"Storage"},"e8d1":{"name":"Store"},"e563":{"name":"Store Mall Directory"},"e41c":{"name":"Straighten"},"e56e":{"name":"Streetview"},"e257":{"name":"Strikethrough S"},"e41d":{"name":"Style"},"e5d9":{"name":"Subdirectory Arrow Left"},"e5da":{"name":"Subdirectory Arrow Right"},"e8d2":{"name":"Subject"},"e064":{"name":"Subscriptions"},"e048":{"name":"Subtitles"},"e56f":{"name":"Subway"},"e8d3":{"name":"Supervisor Account"},"e049":{"name":"Surround Sound"},"e0d7":{"name":"Swap Calls"},"e8d4":{"name":"Swap Horiz"},"e8d5":{"name":"Swap Vert"},"e8d6":{"name":"Swap Vertical Circle"},"e41e":{"name":"Switch Camera"},"e41f":{"name":"Switch Video"},"e627":{"name":"Sync"},"e628":{"name":"Sync Disabled"},"e629":{"name":"Sync Problem"},"e62a":{"name":"System Update"},"e8d7":{"name":"System Update Alt"},"e8d8":{"name":"Tab"},"e8d9":{"name":"Tab Unselected"},"e32f":{"name":"Tablet"},"e330":{"name":"Tablet Android"},"e331":{"name":"Tablet Mac"},"e420":{"name":"Tag Faces"},"e62b":{"name":"Tap And Play"},"e564":{"name":"Terrain"},"e262":{"name":"Text Fields"},"e165":{"name":"Text Format"},"e0d8":{"name":"Textsms"},"e421":{"name":"Texture"},"e8da":{"name":"Theaters"},"e8db":{"name":"Thumb Down"},"e8dc":{"name":"Thumb Up"},"e8dd":{"name":"Thumbs Up Down"},"e62c":{"name":"Time To Leave"},"e422":{"name":"Timelapse"},"e922":{"name":"Timeline"},"e425":{"name":"Timer"},"e423":{"name":"Timer 10"},"e424":{"name":"Timer 3"},"e426":{"name":"Timer Off"},"e264":{"name":"Title"},"e8de":{"name":"Toc"},"e8df":{"name":"Today"},"e8e0":{"name":"Toll"},"e427":{"name":"Tonality"},"e913":{"name":"Touch App"},"e332":{"name":"Toys"},"e8e1":{"name":"Track Changes"},"e565":{"name":"Traffic"},"e570":{"name":"Train"},"e571":{"name":"Tram"},"e572":{"name":"Transfer Within A Station"},"e428":{"name":"Transform"},"e8e2":{"name":"Translate"},"e8e3":{"name":"Trending Down"},"e8e4":{"name":"Trending Flat"},"e8e5":{"name":"Trending Up"},"e429":{"name":"Tune"},"e8e6":{"name":"Turned In"},"e8e7":{"name":"Turned In Not"},"e333":{"name":"Tv"},"e169":{"name":"Unarchive"},"e166":{"name":"Undo"},"e5d6":{"name":"Unfold Less"},"e5d7":{"name":"Unfold More"},"e923":{"name":"Update"},"e1e0":{"name":"Usb"},"e8e8":{"name":"Verified User"},"e258":{"name":"Vertical Align Bottom"},"e259":{"name":"Vertical Align Center"},"e25a":{"name":"Vertical Align Top"},"e62d":{"name":"Vibration"},"e070":{"name":"Video Call"},"e071":{"name":"Video Label"},"e04a":{"name":"Video Library"},"e04b":{"name":"Videocam"},"e04c":{"name":"Videocam Off"},"e338":{"name":"Videogame Asset"},"e8e9":{"name":"View Agenda"},"e8ea":{"name":"View Array"},"e8eb":{"name":"View Carousel"},"e8ec":{"name":"View Column"},"e42a":{"name":"View Comfy"},"e42b":{"name":"View Compact"},"e8ed":{"name":"View Day"},"e8ee":{"name":"View Headline"},"e8ef":{"name":"View List"},"e8f0":{"name":"View Module"},"e8f1":{"name":"View Quilt"},"e8f2":{"name":"View Stream"},"e8f3":{"name":"View Week"},"e435":{"name":"Vignette"},"e8f4":{"name":"Visibility"},"e8f5":{"name":"Visibility Off"},"e62e":{"name":"Voice Chat"},"e0d9":{"name":"Voicemail"},"e04d":{"name":"Volume Down"},"e04e":{"name":"Volume Mute"},"e04f":{"name":"Volume Off"},"e050":{"name":"Volume Up"},"e0da":{"name":"Vpn Key"},"e62f":{"name":"Vpn Lock"},"e1bc":{"name":"Wallpaper"},"e002":{"name":"Warning"},"e334":{"name":"Watch"},"e924":{"name":"Watch Later"},"e42c":{"name":"Wb Auto"},"e42d":{"name":"Wb Cloudy"},"e42e":{"name":"Wb Incandescent"},"e436":{"name":"Wb Iridescent"},"e430":{"name":"Wb Sunny"},"e63d":{"name":"Wc"},"e051":{"name":"Web"},"e069":{"name":"Web Asset"},"e16b":{"name":"Weekend"},"e80e":{"name":"Whatshot"},"e1bd":{"name":"Widgets"},"e63e":{"name":"Wifi"},"e1e1":{"name":"Wifi Lock"},"e1e2":{"name":"Wifi Tethering"},"e8f9":{"name":"Work"},"e25b":{"name":"Wrap Text"},"e8fa":{"name":"Youtube Searched For"},"e8ff":{"name":"Zoom In"},"e900":{"name":"Zoom Out"},"e56b":{"name":"Zoom Out Map"}}} -------------------------------------------------------------------------------- /assets/fonts/material/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanwald/checkmarks/8ea20e2fdb756bf0d6a790eb0110b24732548329/assets/fonts/material/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/material/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanwald/checkmarks/8ea20e2fdb756bf0d6a790eb0110b24732548329/assets/fonts/material/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /assets/fonts/material/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanwald/checkmarks/8ea20e2fdb756bf0d6a790eb0110b24732548329/assets/fonts/material/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /assets/fonts/material/codepoints: -------------------------------------------------------------------------------- 1 | 3d_rotation e84d 2 | ac_unit eb3b 3 | access_alarm e190 4 | access_alarms e191 5 | access_time e192 6 | accessibility e84e 7 | accessible e914 8 | account_balance e84f 9 | account_balance_wallet e850 10 | account_box e851 11 | account_circle e853 12 | adb e60e 13 | add e145 14 | add_a_photo e439 15 | add_alarm e193 16 | add_alert e003 17 | add_box e146 18 | add_circle e147 19 | add_circle_outline e148 20 | add_location e567 21 | add_shopping_cart e854 22 | add_to_photos e39d 23 | add_to_queue e05c 24 | adjust e39e 25 | airline_seat_flat e630 26 | airline_seat_flat_angled e631 27 | airline_seat_individual_suite e632 28 | airline_seat_legroom_extra e633 29 | airline_seat_legroom_normal e634 30 | airline_seat_legroom_reduced e635 31 | airline_seat_recline_extra e636 32 | airline_seat_recline_normal e637 33 | airplanemode_active e195 34 | airplanemode_inactive e194 35 | airplay e055 36 | airport_shuttle eb3c 37 | alarm e855 38 | alarm_add e856 39 | alarm_off e857 40 | alarm_on e858 41 | album e019 42 | all_inclusive eb3d 43 | all_out e90b 44 | android e859 45 | announcement e85a 46 | apps e5c3 47 | archive e149 48 | arrow_back e5c4 49 | arrow_downward e5db 50 | arrow_drop_down e5c5 51 | arrow_drop_down_circle e5c6 52 | arrow_drop_up e5c7 53 | arrow_forward e5c8 54 | arrow_upward e5d8 55 | art_track e060 56 | aspect_ratio e85b 57 | assessment e85c 58 | assignment e85d 59 | assignment_ind e85e 60 | assignment_late e85f 61 | assignment_return e860 62 | assignment_returned e861 63 | assignment_turned_in e862 64 | assistant e39f 65 | assistant_photo e3a0 66 | attach_file e226 67 | attach_money e227 68 | attachment e2bc 69 | audiotrack e3a1 70 | autorenew e863 71 | av_timer e01b 72 | backspace e14a 73 | backup e864 74 | battery_alert e19c 75 | battery_charging_full e1a3 76 | battery_full e1a4 77 | battery_std e1a5 78 | battery_unknown e1a6 79 | beach_access eb3e 80 | beenhere e52d 81 | block e14b 82 | bluetooth e1a7 83 | bluetooth_audio e60f 84 | bluetooth_connected e1a8 85 | bluetooth_disabled e1a9 86 | bluetooth_searching e1aa 87 | blur_circular e3a2 88 | blur_linear e3a3 89 | blur_off e3a4 90 | blur_on e3a5 91 | book e865 92 | bookmark e866 93 | bookmark_border e867 94 | border_all e228 95 | border_bottom e229 96 | border_clear e22a 97 | border_color e22b 98 | border_horizontal e22c 99 | border_inner e22d 100 | border_left e22e 101 | border_outer e22f 102 | border_right e230 103 | border_style e231 104 | border_top e232 105 | border_vertical e233 106 | branding_watermark e06b 107 | brightness_1 e3a6 108 | brightness_2 e3a7 109 | brightness_3 e3a8 110 | brightness_4 e3a9 111 | brightness_5 e3aa 112 | brightness_6 e3ab 113 | brightness_7 e3ac 114 | brightness_auto e1ab 115 | brightness_high e1ac 116 | brightness_low e1ad 117 | brightness_medium e1ae 118 | broken_image e3ad 119 | brush e3ae 120 | bubble_chart e6dd 121 | bug_report e868 122 | build e869 123 | burst_mode e43c 124 | business e0af 125 | business_center eb3f 126 | cached e86a 127 | cake e7e9 128 | call e0b0 129 | call_end e0b1 130 | call_made e0b2 131 | call_merge e0b3 132 | call_missed e0b4 133 | call_missed_outgoing e0e4 134 | call_received e0b5 135 | call_split e0b6 136 | call_to_action e06c 137 | camera e3af 138 | camera_alt e3b0 139 | camera_enhance e8fc 140 | camera_front e3b1 141 | camera_rear e3b2 142 | camera_roll e3b3 143 | cancel e5c9 144 | card_giftcard e8f6 145 | card_membership e8f7 146 | card_travel e8f8 147 | casino eb40 148 | cast e307 149 | cast_connected e308 150 | center_focus_strong e3b4 151 | center_focus_weak e3b5 152 | change_history e86b 153 | chat e0b7 154 | chat_bubble e0ca 155 | chat_bubble_outline e0cb 156 | check e5ca 157 | check_box e834 158 | check_box_outline_blank e835 159 | check_circle e86c 160 | chevron_left e5cb 161 | chevron_right e5cc 162 | child_care eb41 163 | child_friendly eb42 164 | chrome_reader_mode e86d 165 | class e86e 166 | clear e14c 167 | clear_all e0b8 168 | close e5cd 169 | closed_caption e01c 170 | cloud e2bd 171 | cloud_circle e2be 172 | cloud_done e2bf 173 | cloud_download e2c0 174 | cloud_off e2c1 175 | cloud_queue e2c2 176 | cloud_upload e2c3 177 | code e86f 178 | collections e3b6 179 | collections_bookmark e431 180 | color_lens e3b7 181 | colorize e3b8 182 | comment e0b9 183 | compare e3b9 184 | compare_arrows e915 185 | computer e30a 186 | confirmation_number e638 187 | contact_mail e0d0 188 | contact_phone e0cf 189 | contacts e0ba 190 | content_copy e14d 191 | content_cut e14e 192 | content_paste e14f 193 | control_point e3ba 194 | control_point_duplicate e3bb 195 | copyright e90c 196 | create e150 197 | create_new_folder e2cc 198 | credit_card e870 199 | crop e3be 200 | crop_16_9 e3bc 201 | crop_3_2 e3bd 202 | crop_5_4 e3bf 203 | crop_7_5 e3c0 204 | crop_din e3c1 205 | crop_free e3c2 206 | crop_landscape e3c3 207 | crop_original e3c4 208 | crop_portrait e3c5 209 | crop_rotate e437 210 | crop_square e3c6 211 | dashboard e871 212 | data_usage e1af 213 | date_range e916 214 | dehaze e3c7 215 | delete e872 216 | delete_forever e92b 217 | delete_sweep e16c 218 | description e873 219 | desktop_mac e30b 220 | desktop_windows e30c 221 | details e3c8 222 | developer_board e30d 223 | developer_mode e1b0 224 | device_hub e335 225 | devices e1b1 226 | devices_other e337 227 | dialer_sip e0bb 228 | dialpad e0bc 229 | directions e52e 230 | directions_bike e52f 231 | directions_boat e532 232 | directions_bus e530 233 | directions_car e531 234 | directions_railway e534 235 | directions_run e566 236 | directions_subway e533 237 | directions_transit e535 238 | directions_walk e536 239 | disc_full e610 240 | dns e875 241 | do_not_disturb e612 242 | do_not_disturb_alt e611 243 | do_not_disturb_off e643 244 | do_not_disturb_on e644 245 | dock e30e 246 | domain e7ee 247 | done e876 248 | done_all e877 249 | donut_large e917 250 | donut_small e918 251 | drafts e151 252 | drag_handle e25d 253 | drive_eta e613 254 | dvr e1b2 255 | edit e3c9 256 | edit_location e568 257 | eject e8fb 258 | email e0be 259 | enhanced_encryption e63f 260 | equalizer e01d 261 | error e000 262 | error_outline e001 263 | euro_symbol e926 264 | ev_station e56d 265 | event e878 266 | event_available e614 267 | event_busy e615 268 | event_note e616 269 | event_seat e903 270 | exit_to_app e879 271 | expand_less e5ce 272 | expand_more e5cf 273 | explicit e01e 274 | explore e87a 275 | exposure e3ca 276 | exposure_neg_1 e3cb 277 | exposure_neg_2 e3cc 278 | exposure_plus_1 e3cd 279 | exposure_plus_2 e3ce 280 | exposure_zero e3cf 281 | extension e87b 282 | face e87c 283 | fast_forward e01f 284 | fast_rewind e020 285 | favorite e87d 286 | favorite_border e87e 287 | featured_play_list e06d 288 | featured_video e06e 289 | feedback e87f 290 | fiber_dvr e05d 291 | fiber_manual_record e061 292 | fiber_new e05e 293 | fiber_pin e06a 294 | fiber_smart_record e062 295 | file_download e2c4 296 | file_upload e2c6 297 | filter e3d3 298 | filter_1 e3d0 299 | filter_2 e3d1 300 | filter_3 e3d2 301 | filter_4 e3d4 302 | filter_5 e3d5 303 | filter_6 e3d6 304 | filter_7 e3d7 305 | filter_8 e3d8 306 | filter_9 e3d9 307 | filter_9_plus e3da 308 | filter_b_and_w e3db 309 | filter_center_focus e3dc 310 | filter_drama e3dd 311 | filter_frames e3de 312 | filter_hdr e3df 313 | filter_list e152 314 | filter_none e3e0 315 | filter_tilt_shift e3e2 316 | filter_vintage e3e3 317 | find_in_page e880 318 | find_replace e881 319 | fingerprint e90d 320 | first_page e5dc 321 | fitness_center eb43 322 | flag e153 323 | flare e3e4 324 | flash_auto e3e5 325 | flash_off e3e6 326 | flash_on e3e7 327 | flight e539 328 | flight_land e904 329 | flight_takeoff e905 330 | flip e3e8 331 | flip_to_back e882 332 | flip_to_front e883 333 | folder e2c7 334 | folder_open e2c8 335 | folder_shared e2c9 336 | folder_special e617 337 | font_download e167 338 | format_align_center e234 339 | format_align_justify e235 340 | format_align_left e236 341 | format_align_right e237 342 | format_bold e238 343 | format_clear e239 344 | format_color_fill e23a 345 | format_color_reset e23b 346 | format_color_text e23c 347 | format_indent_decrease e23d 348 | format_indent_increase e23e 349 | format_italic e23f 350 | format_line_spacing e240 351 | format_list_bulleted e241 352 | format_list_numbered e242 353 | format_paint e243 354 | format_quote e244 355 | format_shapes e25e 356 | format_size e245 357 | format_strikethrough e246 358 | format_textdirection_l_to_r e247 359 | format_textdirection_r_to_l e248 360 | format_underlined e249 361 | forum e0bf 362 | forward e154 363 | forward_10 e056 364 | forward_30 e057 365 | forward_5 e058 366 | free_breakfast eb44 367 | fullscreen e5d0 368 | fullscreen_exit e5d1 369 | functions e24a 370 | g_translate e927 371 | gamepad e30f 372 | games e021 373 | gavel e90e 374 | gesture e155 375 | get_app e884 376 | gif e908 377 | golf_course eb45 378 | gps_fixed e1b3 379 | gps_not_fixed e1b4 380 | gps_off e1b5 381 | grade e885 382 | gradient e3e9 383 | grain e3ea 384 | graphic_eq e1b8 385 | grid_off e3eb 386 | grid_on e3ec 387 | group e7ef 388 | group_add e7f0 389 | group_work e886 390 | hd e052 391 | hdr_off e3ed 392 | hdr_on e3ee 393 | hdr_strong e3f1 394 | hdr_weak e3f2 395 | headset e310 396 | headset_mic e311 397 | healing e3f3 398 | hearing e023 399 | help e887 400 | help_outline e8fd 401 | high_quality e024 402 | highlight e25f 403 | highlight_off e888 404 | history e889 405 | home e88a 406 | hot_tub eb46 407 | hotel e53a 408 | hourglass_empty e88b 409 | hourglass_full e88c 410 | http e902 411 | https e88d 412 | image e3f4 413 | image_aspect_ratio e3f5 414 | import_contacts e0e0 415 | import_export e0c3 416 | important_devices e912 417 | inbox e156 418 | indeterminate_check_box e909 419 | info e88e 420 | info_outline e88f 421 | input e890 422 | insert_chart e24b 423 | insert_comment e24c 424 | insert_drive_file e24d 425 | insert_emoticon e24e 426 | insert_invitation e24f 427 | insert_link e250 428 | insert_photo e251 429 | invert_colors e891 430 | invert_colors_off e0c4 431 | iso e3f6 432 | keyboard e312 433 | keyboard_arrow_down e313 434 | keyboard_arrow_left e314 435 | keyboard_arrow_right e315 436 | keyboard_arrow_up e316 437 | keyboard_backspace e317 438 | keyboard_capslock e318 439 | keyboard_hide e31a 440 | keyboard_return e31b 441 | keyboard_tab e31c 442 | keyboard_voice e31d 443 | kitchen eb47 444 | label e892 445 | label_outline e893 446 | landscape e3f7 447 | language e894 448 | laptop e31e 449 | laptop_chromebook e31f 450 | laptop_mac e320 451 | laptop_windows e321 452 | last_page e5dd 453 | launch e895 454 | layers e53b 455 | layers_clear e53c 456 | leak_add e3f8 457 | leak_remove e3f9 458 | lens e3fa 459 | library_add e02e 460 | library_books e02f 461 | library_music e030 462 | lightbulb_outline e90f 463 | line_style e919 464 | line_weight e91a 465 | linear_scale e260 466 | link e157 467 | linked_camera e438 468 | list e896 469 | live_help e0c6 470 | live_tv e639 471 | local_activity e53f 472 | local_airport e53d 473 | local_atm e53e 474 | local_bar e540 475 | local_cafe e541 476 | local_car_wash e542 477 | local_convenience_store e543 478 | local_dining e556 479 | local_drink e544 480 | local_florist e545 481 | local_gas_station e546 482 | local_grocery_store e547 483 | local_hospital e548 484 | local_hotel e549 485 | local_laundry_service e54a 486 | local_library e54b 487 | local_mall e54c 488 | local_movies e54d 489 | local_offer e54e 490 | local_parking e54f 491 | local_pharmacy e550 492 | local_phone e551 493 | local_pizza e552 494 | local_play e553 495 | local_post_office e554 496 | local_printshop e555 497 | local_see e557 498 | local_shipping e558 499 | local_taxi e559 500 | location_city e7f1 501 | location_disabled e1b6 502 | location_off e0c7 503 | location_on e0c8 504 | location_searching e1b7 505 | lock e897 506 | lock_open e898 507 | lock_outline e899 508 | looks e3fc 509 | looks_3 e3fb 510 | looks_4 e3fd 511 | looks_5 e3fe 512 | looks_6 e3ff 513 | looks_one e400 514 | looks_two e401 515 | loop e028 516 | loupe e402 517 | low_priority e16d 518 | loyalty e89a 519 | mail e158 520 | mail_outline e0e1 521 | map e55b 522 | markunread e159 523 | markunread_mailbox e89b 524 | memory e322 525 | menu e5d2 526 | merge_type e252 527 | message e0c9 528 | mic e029 529 | mic_none e02a 530 | mic_off e02b 531 | mms e618 532 | mode_comment e253 533 | mode_edit e254 534 | monetization_on e263 535 | money_off e25c 536 | monochrome_photos e403 537 | mood e7f2 538 | mood_bad e7f3 539 | more e619 540 | more_horiz e5d3 541 | more_vert e5d4 542 | motorcycle e91b 543 | mouse e323 544 | move_to_inbox e168 545 | movie e02c 546 | movie_creation e404 547 | movie_filter e43a 548 | multiline_chart e6df 549 | music_note e405 550 | music_video e063 551 | my_location e55c 552 | nature e406 553 | nature_people e407 554 | navigate_before e408 555 | navigate_next e409 556 | navigation e55d 557 | near_me e569 558 | network_cell e1b9 559 | network_check e640 560 | network_locked e61a 561 | network_wifi e1ba 562 | new_releases e031 563 | next_week e16a 564 | nfc e1bb 565 | no_encryption e641 566 | no_sim e0cc 567 | not_interested e033 568 | note e06f 569 | note_add e89c 570 | notifications e7f4 571 | notifications_active e7f7 572 | notifications_none e7f5 573 | notifications_off e7f6 574 | notifications_paused e7f8 575 | offline_pin e90a 576 | ondemand_video e63a 577 | opacity e91c 578 | open_in_browser e89d 579 | open_in_new e89e 580 | open_with e89f 581 | pages e7f9 582 | pageview e8a0 583 | palette e40a 584 | pan_tool e925 585 | panorama e40b 586 | panorama_fish_eye e40c 587 | panorama_horizontal e40d 588 | panorama_vertical e40e 589 | panorama_wide_angle e40f 590 | party_mode e7fa 591 | pause e034 592 | pause_circle_filled e035 593 | pause_circle_outline e036 594 | payment e8a1 595 | people e7fb 596 | people_outline e7fc 597 | perm_camera_mic e8a2 598 | perm_contact_calendar e8a3 599 | perm_data_setting e8a4 600 | perm_device_information e8a5 601 | perm_identity e8a6 602 | perm_media e8a7 603 | perm_phone_msg e8a8 604 | perm_scan_wifi e8a9 605 | person e7fd 606 | person_add e7fe 607 | person_outline e7ff 608 | person_pin e55a 609 | person_pin_circle e56a 610 | personal_video e63b 611 | pets e91d 612 | phone e0cd 613 | phone_android e324 614 | phone_bluetooth_speaker e61b 615 | phone_forwarded e61c 616 | phone_in_talk e61d 617 | phone_iphone e325 618 | phone_locked e61e 619 | phone_missed e61f 620 | phone_paused e620 621 | phonelink e326 622 | phonelink_erase e0db 623 | phonelink_lock e0dc 624 | phonelink_off e327 625 | phonelink_ring e0dd 626 | phonelink_setup e0de 627 | photo e410 628 | photo_album e411 629 | photo_camera e412 630 | photo_filter e43b 631 | photo_library e413 632 | photo_size_select_actual e432 633 | photo_size_select_large e433 634 | photo_size_select_small e434 635 | picture_as_pdf e415 636 | picture_in_picture e8aa 637 | picture_in_picture_alt e911 638 | pie_chart e6c4 639 | pie_chart_outlined e6c5 640 | pin_drop e55e 641 | place e55f 642 | play_arrow e037 643 | play_circle_filled e038 644 | play_circle_outline e039 645 | play_for_work e906 646 | playlist_add e03b 647 | playlist_add_check e065 648 | playlist_play e05f 649 | plus_one e800 650 | poll e801 651 | polymer e8ab 652 | pool eb48 653 | portable_wifi_off e0ce 654 | portrait e416 655 | power e63c 656 | power_input e336 657 | power_settings_new e8ac 658 | pregnant_woman e91e 659 | present_to_all e0df 660 | print e8ad 661 | priority_high e645 662 | public e80b 663 | publish e255 664 | query_builder e8ae 665 | question_answer e8af 666 | queue e03c 667 | queue_music e03d 668 | queue_play_next e066 669 | radio e03e 670 | radio_button_checked e837 671 | radio_button_unchecked e836 672 | rate_review e560 673 | receipt e8b0 674 | recent_actors e03f 675 | record_voice_over e91f 676 | redeem e8b1 677 | redo e15a 678 | refresh e5d5 679 | remove e15b 680 | remove_circle e15c 681 | remove_circle_outline e15d 682 | remove_from_queue e067 683 | remove_red_eye e417 684 | remove_shopping_cart e928 685 | reorder e8fe 686 | repeat e040 687 | repeat_one e041 688 | replay e042 689 | replay_10 e059 690 | replay_30 e05a 691 | replay_5 e05b 692 | reply e15e 693 | reply_all e15f 694 | report e160 695 | report_problem e8b2 696 | restaurant e56c 697 | restaurant_menu e561 698 | restore e8b3 699 | restore_page e929 700 | ring_volume e0d1 701 | room e8b4 702 | room_service eb49 703 | rotate_90_degrees_ccw e418 704 | rotate_left e419 705 | rotate_right e41a 706 | rounded_corner e920 707 | router e328 708 | rowing e921 709 | rss_feed e0e5 710 | rv_hookup e642 711 | satellite e562 712 | save e161 713 | scanner e329 714 | schedule e8b5 715 | school e80c 716 | screen_lock_landscape e1be 717 | screen_lock_portrait e1bf 718 | screen_lock_rotation e1c0 719 | screen_rotation e1c1 720 | screen_share e0e2 721 | sd_card e623 722 | sd_storage e1c2 723 | search e8b6 724 | security e32a 725 | select_all e162 726 | send e163 727 | sentiment_dissatisfied e811 728 | sentiment_neutral e812 729 | sentiment_satisfied e813 730 | sentiment_very_dissatisfied e814 731 | sentiment_very_satisfied e815 732 | settings e8b8 733 | settings_applications e8b9 734 | settings_backup_restore e8ba 735 | settings_bluetooth e8bb 736 | settings_brightness e8bd 737 | settings_cell e8bc 738 | settings_ethernet e8be 739 | settings_input_antenna e8bf 740 | settings_input_component e8c0 741 | settings_input_composite e8c1 742 | settings_input_hdmi e8c2 743 | settings_input_svideo e8c3 744 | settings_overscan e8c4 745 | settings_phone e8c5 746 | settings_power e8c6 747 | settings_remote e8c7 748 | settings_system_daydream e1c3 749 | settings_voice e8c8 750 | share e80d 751 | shop e8c9 752 | shop_two e8ca 753 | shopping_basket e8cb 754 | shopping_cart e8cc 755 | short_text e261 756 | show_chart e6e1 757 | shuffle e043 758 | signal_cellular_4_bar e1c8 759 | signal_cellular_connected_no_internet_4_bar e1cd 760 | signal_cellular_no_sim e1ce 761 | signal_cellular_null e1cf 762 | signal_cellular_off e1d0 763 | signal_wifi_4_bar e1d8 764 | signal_wifi_4_bar_lock e1d9 765 | signal_wifi_off e1da 766 | sim_card e32b 767 | sim_card_alert e624 768 | skip_next e044 769 | skip_previous e045 770 | slideshow e41b 771 | slow_motion_video e068 772 | smartphone e32c 773 | smoke_free eb4a 774 | smoking_rooms eb4b 775 | sms e625 776 | sms_failed e626 777 | snooze e046 778 | sort e164 779 | sort_by_alpha e053 780 | spa eb4c 781 | space_bar e256 782 | speaker e32d 783 | speaker_group e32e 784 | speaker_notes e8cd 785 | speaker_notes_off e92a 786 | speaker_phone e0d2 787 | spellcheck e8ce 788 | star e838 789 | star_border e83a 790 | star_half e839 791 | stars e8d0 792 | stay_current_landscape e0d3 793 | stay_current_portrait e0d4 794 | stay_primary_landscape e0d5 795 | stay_primary_portrait e0d6 796 | stop e047 797 | stop_screen_share e0e3 798 | storage e1db 799 | store e8d1 800 | store_mall_directory e563 801 | straighten e41c 802 | streetview e56e 803 | strikethrough_s e257 804 | style e41d 805 | subdirectory_arrow_left e5d9 806 | subdirectory_arrow_right e5da 807 | subject e8d2 808 | subscriptions e064 809 | subtitles e048 810 | subway e56f 811 | supervisor_account e8d3 812 | surround_sound e049 813 | swap_calls e0d7 814 | swap_horiz e8d4 815 | swap_vert e8d5 816 | swap_vertical_circle e8d6 817 | switch_camera e41e 818 | switch_video e41f 819 | sync e627 820 | sync_disabled e628 821 | sync_problem e629 822 | system_update e62a 823 | system_update_alt e8d7 824 | tab e8d8 825 | tab_unselected e8d9 826 | tablet e32f 827 | tablet_android e330 828 | tablet_mac e331 829 | tag_faces e420 830 | tap_and_play e62b 831 | terrain e564 832 | text_fields e262 833 | text_format e165 834 | textsms e0d8 835 | texture e421 836 | theaters e8da 837 | thumb_down e8db 838 | thumb_up e8dc 839 | thumbs_up_down e8dd 840 | time_to_leave e62c 841 | timelapse e422 842 | timeline e922 843 | timer e425 844 | timer_10 e423 845 | timer_3 e424 846 | timer_off e426 847 | title e264 848 | toc e8de 849 | today e8df 850 | toll e8e0 851 | tonality e427 852 | touch_app e913 853 | toys e332 854 | track_changes e8e1 855 | traffic e565 856 | train e570 857 | tram e571 858 | transfer_within_a_station e572 859 | transform e428 860 | translate e8e2 861 | trending_down e8e3 862 | trending_flat e8e4 863 | trending_up e8e5 864 | tune e429 865 | turned_in e8e6 866 | turned_in_not e8e7 867 | tv e333 868 | unarchive e169 869 | undo e166 870 | unfold_less e5d6 871 | unfold_more e5d7 872 | update e923 873 | usb e1e0 874 | verified_user e8e8 875 | vertical_align_bottom e258 876 | vertical_align_center e259 877 | vertical_align_top e25a 878 | vibration e62d 879 | video_call e070 880 | video_label e071 881 | video_library e04a 882 | videocam e04b 883 | videocam_off e04c 884 | videogame_asset e338 885 | view_agenda e8e9 886 | view_array e8ea 887 | view_carousel e8eb 888 | view_column e8ec 889 | view_comfy e42a 890 | view_compact e42b 891 | view_day e8ed 892 | view_headline e8ee 893 | view_list e8ef 894 | view_module e8f0 895 | view_quilt e8f1 896 | view_stream e8f2 897 | view_week e8f3 898 | vignette e435 899 | visibility e8f4 900 | visibility_off e8f5 901 | voice_chat e62e 902 | voicemail e0d9 903 | volume_down e04d 904 | volume_mute e04e 905 | volume_off e04f 906 | volume_up e050 907 | vpn_key e0da 908 | vpn_lock e62f 909 | wallpaper e1bc 910 | warning e002 911 | watch e334 912 | watch_later e924 913 | wb_auto e42c 914 | wb_cloudy e42d 915 | wb_incandescent e42e 916 | wb_iridescent e436 917 | wb_sunny e430 918 | wc e63d 919 | web e051 920 | web_asset e069 921 | weekend e16b 922 | whatshot e80e 923 | widgets e1bd 924 | wifi e63e 925 | wifi_lock e1e1 926 | wifi_tethering e1e2 927 | work e8f9 928 | wrap_text e25b 929 | youtube_searched_for e8fa 930 | zoom_in e8ff 931 | zoom_out e900 932 | zoom_out_map e56b 933 | -------------------------------------------------------------------------------- /assets/images/icon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanwald/checkmarks/8ea20e2fdb756bf0d6a790eb0110b24732548329/assets/images/logo.png -------------------------------------------------------------------------------- /assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /assets/styles/checkmarks-commons.css: -------------------------------------------------------------------------------- 1 | /********************************************************************************************************************** 2 | * CHECKMARKS COMMONS 3 | **********************************************************************************************************************/ 4 | html, body { 5 | padding: 0; 6 | margin: 0; 7 | font-family: sans-serif; 8 | font-size: 14px; 9 | color: #4e4947; 10 | } 11 | 12 | @media (prefers-color-scheme: dark) { 13 | body { 14 | color: #b1b1b3; 15 | } 16 | } 17 | 18 | small { 19 | font-size: 10px; 20 | } -------------------------------------------------------------------------------- /assets/styles/checkmarks-material.css: -------------------------------------------------------------------------------- 1 | /********************************************************************************************************************** 2 | * MATERIAL ICONS 3 | **********************************************************************************************************************/ 4 | @font-face { 5 | font-family: 'Material Icons'; 6 | font-style: normal; 7 | font-weight: 400; 8 | src: url(/assets/fonts/material/MaterialIcons-Regular.eot); 9 | src: local('Material Icons'), 10 | local('MaterialIcons-Regular'), 11 | url(/assets/fonts/material/MaterialIcons-Regular.woff2) format('woff2'), 12 | url(/assets/fonts/material/MaterialIcons-Regular.woff) format('woff'), 13 | url(/assets/fonts/material/MaterialIcons-Regular.ttf) format('truetype'); 14 | } 15 | 16 | .material-icons { 17 | font-family: 'Material Icons'; 18 | font-weight: normal; 19 | font-style: normal; 20 | text-transform: none; 21 | letter-spacing: normal; 22 | word-wrap: normal; 23 | white-space: nowrap; 24 | direction: ltr; 25 | 26 | -webkit-font-smoothing: antialiased; 27 | text-rendering: optimizeLegibility; 28 | moz-osx-font-smoothing: grayscale; 29 | font-feature-settings: 'liga'; 30 | } -------------------------------------------------------------------------------- /commons/checkmarks-commons.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Read-only storage for default values. 3 | * @constructor 4 | */ 5 | function DefaultsStore() { 6 | const TIMEOUT = 20; 7 | const TIMEOUT_OVERRULE = 5; 8 | const MAX_TABS = 5; 9 | const IGNORED_DIRS = ['archive', 'local']; 10 | const IGNORED_DIRS_ACTIVE = false; 11 | const INCLUDED_DIRS = []; 12 | const INCLUDED_DIRS_ACTIVE = false; 13 | const IGNORED_URLS = ['localhost', '192.168.', '172.16.', '10.']; 14 | const IGNORED_URLS_ACTIVE = true; 15 | const SHOW_FAVICONS = true; 16 | const TO_LOWERCASE = false; 17 | const DO_SORT = false; 18 | const SORT_UNFILED_BY_DATE = false; 19 | const CLEAR_CACHE = false; 20 | 21 | this.getTimeout = function () { 22 | return TIMEOUT; 23 | }; 24 | 25 | this.getTimeoutOverrule = function () { 26 | return TIMEOUT_OVERRULE; 27 | }; 28 | 29 | this.getMaxTabs = function () { 30 | return MAX_TABS; 31 | }; 32 | 33 | this.getIgnoredDirs = function () { 34 | return IGNORED_DIRS; 35 | }; 36 | 37 | this.getIgnoredDirsActive = function () { 38 | return IGNORED_DIRS_ACTIVE; 39 | }; 40 | 41 | this.getIncludedDirs = function () { 42 | return INCLUDED_DIRS; 43 | }; 44 | 45 | this.getIncludedDirsActive = function () { 46 | return INCLUDED_DIRS_ACTIVE; 47 | }; 48 | 49 | this.getIgnoredUrls = function () { 50 | return IGNORED_URLS; 51 | }; 52 | 53 | this.getIgnoredUrlsActive = function () { 54 | return IGNORED_URLS_ACTIVE; 55 | }; 56 | 57 | this.getShowFavicons = function () { 58 | return SHOW_FAVICONS; 59 | }; 60 | 61 | this.getToLowercase = function () { 62 | return TO_LOWERCASE; 63 | }; 64 | 65 | this.doSort = function () { 66 | return DO_SORT; 67 | }; 68 | 69 | this.sortUnfiledByDate = function () { 70 | return SORT_UNFILED_BY_DATE; 71 | }; 72 | 73 | this.getClearCache = function () { 74 | return CLEAR_CACHE; 75 | } 76 | } 77 | 78 | const CM_DEFAULTS = new DefaultsStore(); 79 | 80 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Checkmarks", 4 | "description": "Checks, sorts, formats bookmarks and loads favicons.", 5 | "version": "1.6.1", 6 | "icons": { 7 | "48": "assets/images/logo.svg", 8 | "96": "assets/images/logo.svg" 9 | }, 10 | "sidebar_action": { 11 | "default_icon": "assets/images/icon-light.svg", 12 | "default_title": "Checkmarks", 13 | "default_panel": "sidebar/checkmarks-sidebar.html" 14 | }, 15 | "options_ui": { 16 | "page": "options/checkmarks-options.html", 17 | "browser_style": true 18 | }, 19 | "permissions": [ 20 | "", 21 | "bookmarks", 22 | "browsingData", 23 | "storage", 24 | "tabs", 25 | "webNavigation", 26 | "webRequest", 27 | "webRequestBlocking" 28 | ] 29 | } 30 | 31 | 32 | -------------------------------------------------------------------------------- /options/checkmarks-options.css: -------------------------------------------------------------------------------- 1 | /********************************************************************************************************************** 2 | * CHECKMARKS OPTIONS 3 | **********************************************************************************************************************/ 4 | body { 5 | margin: 5px; 6 | } 7 | 8 | @media (prefers-color-scheme: dark) { 9 | body { 10 | background-color: #202023; 11 | } 12 | } 13 | 14 | @media (prefers-color-scheme: dark) { 15 | h1 { 16 | color: #f9f9fa; 17 | } 18 | } 19 | 20 | label { 21 | margin-bottom: 40px; 22 | } 23 | 24 | input { 25 | color: #4e4947; 26 | padding: 5px; 27 | border: 1px solid #b1b1b3; 28 | } 29 | 30 | input:focus { 31 | border: 1px solid #0a84ff; 32 | } 33 | 34 | @media (prefers-color-scheme: dark) { 35 | input { 36 | color: #f9f9fa; 37 | border: 1px solid #4c4c4e; 38 | background-color: #202023; 39 | } 40 | } 41 | 42 | /********************************************************************************************************************** 43 | * INPUT NUMBERS 44 | **********************************************************************************************************************/ 45 | input[type="number"] { 46 | width: 60px; 47 | } 48 | 49 | /********************************************************************************************************************** 50 | * INPUT TEXT 51 | **********************************************************************************************************************/ 52 | input[type="text"] { 53 | width: 100%; 54 | margin-top: 4px; 55 | } 56 | 57 | /********************************************************************************************************************** 58 | * INPUT CHECKBOX 59 | **********************************************************************************************************************/ 60 | input[type="checkbox"] { 61 | vertical-align: middle; 62 | } -------------------------------------------------------------------------------- /options/checkmarks-options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 23 | 31 | 39 | 48 | 56 | 57 | 58 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /options/checkmarks-options.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class for the options page. 3 | * @constructor 4 | */ 5 | function CheckmarksOptions() { 6 | 7 | const requestTimeoutInput = document.getElementById('request-timeout'); 8 | const timeoutOverruleInput = document.getElementById('timeout-overrule'); 9 | const maxTabsInput = document.getElementById('max-tabs'); 10 | const ignoredDirsInput = document.getElementById('ignored-dirs'); 11 | const ignoredDirsCheckbox = document.getElementById('ignored-dirs-active'); 12 | const includedDirsInput = document.getElementById('included-dirs'); 13 | const includedDirsCheckbox = document.getElementById('included-dirs-active'); 14 | const ignoredUrlsInput = document.getElementById('ignored-urls'); 15 | const ignoredUrlsCheckbox = document.getElementById('ignored-urls-active'); 16 | const showFaviconsCheckbox = document.getElementById('show-favicons'); 17 | const toLowercaseCheckbox = document.getElementById('to-lowercase'); 18 | const doSort = document.getElementById('sort'); 19 | const sortUnfiledByDate = document.getElementById('sort-unfiled-by-date'); 20 | const clearCacheCheckbox = document.getElementById('clear-cache'); 21 | 22 | /** 23 | * Registers event-listeners to store options on input and retrieve them on load. 24 | */ 25 | this.init = function () { 26 | document.addEventListener('DOMContentLoaded', restoreOptions); 27 | document.querySelectorAll('input').forEach((input) => { 28 | input.addEventListener('input', setOptions); 29 | }); 30 | }; 31 | 32 | /** 33 | * Updates all stored options with current values. K.I.S.S... 34 | */ 35 | let setOptions = function () { 36 | browser.storage.local.set({ 37 | options: { 38 | requestTimeout: requestTimeoutInput.value, 39 | timeoutOverrule: timeoutOverruleInput.value, 40 | maxTabs: maxTabsInput.value, 41 | ignoredDirs: ignoredDirsInput.value, 42 | ignoredDirsActive: ignoredDirsCheckbox.checked, 43 | includedDirs: includedDirsInput.value, 44 | includedDirsActive: includedDirsCheckbox.checked, 45 | ignoredUrls: ignoredUrlsInput.value, 46 | ignoredUrlsActive: ignoredUrlsCheckbox.checked, 47 | showFavicons: showFaviconsCheckbox.checked, 48 | toLowercase: toLowercaseCheckbox.checked, 49 | doSort: doSort.checked, 50 | sortUnfiledByDate: sortUnfiledByDate.checked, 51 | clearCache: clearCacheCheckbox.checked 52 | } 53 | }); 54 | }; 55 | 56 | /** 57 | * Restores options from local storage or sets default values. 58 | */ 59 | let restoreOptions = function () { 60 | browser.storage.local.get() 61 | .then((storage) => { 62 | const options = storage.options || {}; 63 | requestTimeoutInput.value = options.requestTimeout || CM_DEFAULTS.getTimeout(); 64 | timeoutOverruleInput.value = options.timeoutOverrule || CM_DEFAULTS.getTimeoutOverrule(); 65 | maxTabsInput.value = options.maxTabs || CM_DEFAULTS.getMaxTabs(); 66 | ignoredDirsInput.value = options.ignoredDirs || CM_DEFAULTS.getIgnoredDirs(); 67 | ignoredDirsCheckbox.checked = options.ignoredDirsActive || CM_DEFAULTS.getIgnoredDirsActive(); 68 | includedDirsInput.value = options.includedDirs || CM_DEFAULTS.getIncludedDirs(); 69 | includedDirsCheckbox.checked = options.includedDirsActive || CM_DEFAULTS.getIncludedDirsActive(); 70 | ignoredUrlsInput.value = options.ignoredUrls || CM_DEFAULTS.getIgnoredUrls(); 71 | ignoredUrlsCheckbox.checked = options.ignoredUrlsActive || CM_DEFAULTS.getIgnoredUrlsActive(); 72 | showFaviconsCheckbox.checked = options.showFavicons || CM_DEFAULTS.getShowFavicons(); 73 | toLowercaseCheckbox.checked = options.toLowercase || CM_DEFAULTS.getToLowercase(); 74 | doSort.checked = options.doSort || CM_DEFAULTS.doSort(); 75 | sortUnfiledByDate.checked = options.sortUnfiledByDate || CM_DEFAULTS.sortUnfiledByDate(); 76 | clearCacheCheckbox.checked = options.clearCache || CM_DEFAULTS.getClearCache(); 77 | }); 78 | }; 79 | } 80 | 81 | const checkmarksOptions = new CheckmarksOptions(); 82 | checkmarksOptions.init(); 83 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ "$1" = "build" ]; then 4 | web-ext lint && web-ext build --overwrite-dest --ignore-files=run.sh --ignore-files=Checkmarks.iml 5 | else 6 | if [ "$1" = "dark" ]; then 7 | gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita-dark' 8 | elif [ "$1" = "light" ]; then 9 | gsettings set org.gnome.desktop.interface gtk-theme 'Adwaita' 10 | fi 11 | web-ext run --reload --url 'about:debugging#/runtime/this-firefox' 12 | fi -------------------------------------------------------------------------------- /sidebar/checkmarks-sidebar.css: -------------------------------------------------------------------------------- 1 | /********************************************************************************************************************** 2 | * CHECKMARKS SIDEBAR 3 | **********************************************************************************************************************/ 4 | 5 | @media (prefers-color-scheme: dark) { 6 | body { 7 | background-color: #2d2d2d; 8 | } 9 | } 10 | 11 | /********************************************************************************************************************** 12 | * TOP LEVEL CONTROLS/INDICATORS 13 | **********************************************************************************************************************/ 14 | #content { 15 | position: relative; 16 | } 17 | 18 | header { 19 | position: fixed; 20 | z-index: 1; 21 | top: 0; 22 | width: 100%; 23 | } 24 | 25 | #controls, 26 | #progress { 27 | position: absolute; 28 | z-index: 2; 29 | top: 0; 30 | height: 70px; 31 | line-height: 70px; 32 | } 33 | 34 | #controls { 35 | font-size: 26px; 36 | left: 20px; 37 | } 38 | 39 | #progress { 40 | right: 20px; 41 | font-weight: bold; 42 | } 43 | 44 | #apostrophe { 45 | position: absolute; 46 | z-index: 2; 47 | top: 5px; 48 | left: 20px; 49 | font-size: 10px; 50 | } 51 | 52 | @media (prefers-color-scheme: dark) { 53 | #controls, 54 | #progress, 55 | #apostrophe { 56 | color: #f9f9fa; 57 | } 58 | } 59 | 60 | /********************************************************************************************************************** 61 | * PROGRESS BAR 62 | **********************************************************************************************************************/ 63 | #progress-container { 64 | height: 70px; 65 | width: 100%; 66 | border-bottom: 1px solid #b1b1b3; 67 | background-color: rgba(241, 242, 242, .9); 68 | } 69 | 70 | @media (prefers-color-scheme: dark) { 71 | #progress-container { 72 | background-color: #2d2d2d; 73 | } 74 | } 75 | 76 | #progress-bar { 77 | height: 100%; 78 | width: 0; 79 | background-color: #497eb8; 80 | } 81 | 82 | /********************************************************************************************************************** 83 | * STATISTICS/FILTER 84 | **********************************************************************************************************************/ 85 | #statistics, 86 | #filters { 87 | display: none; 88 | overflow: hidden; 89 | white-space: nowrap; 90 | text-overflow: ellipsis; 91 | border-bottom: 1px solid #b1b1b3; 92 | background-color: rgba(241, 242, 242, .9); 93 | padding: 0 20px; 94 | line-height: 40px; 95 | font-size: 12px; 96 | } 97 | 98 | @media (prefers-color-scheme: dark) { 99 | #statistics, 100 | #filters { 101 | background-color: #2d2d2d; 102 | } 103 | } 104 | 105 | /********************************************************************************************************************** 106 | * FAVICONS 107 | **********************************************************************************************************************/ 108 | #favicons { 109 | display: none; 110 | overflow: hidden; 111 | white-space: nowrap; 112 | height: 40px; 113 | padding: 10px 0; 114 | border-bottom: 1px solid #b1b1b3; 115 | background-color: rgba(255, 255, 255, .9); 116 | } 117 | 118 | 119 | @media (prefers-color-scheme: dark) { 120 | #favicons { 121 | background-color: #2d2d2d; 122 | } 123 | } 124 | 125 | .favicon { 126 | height: 20px; 127 | width: auto; 128 | margin-right: 4px; 129 | } 130 | 131 | /********************************************************************************************************************** 132 | * ICONS 133 | **********************************************************************************************************************/ 134 | .icon { 135 | vertical-align: middle; 136 | margin-right: 6px; 137 | } 138 | 139 | .button { 140 | cursor: pointer; 141 | } 142 | 143 | .success { 144 | color: #12bc00; 145 | } 146 | 147 | .error { 148 | color: #ff9400; 149 | } 150 | 151 | /********************************************************************************************************************** 152 | * MODAL 153 | **********************************************************************************************************************/ 154 | #modal { 155 | display: none; 156 | position: fixed; 157 | z-index: 3; 158 | padding-top: 60px; 159 | left: 0; 160 | top: 0; 161 | width: 100%; 162 | height: 100%; 163 | overflow: auto; 164 | background-color: rgba(0, 0, 0, .6); 165 | } 166 | 167 | .modal-content, 168 | .modal-content p { 169 | text-align: center; 170 | } 171 | 172 | .modal-content { 173 | display: none; 174 | width: 80%; 175 | margin: auto; 176 | padding: 20px; 177 | border: 1px solid #b1b1b3; 178 | background-color: #fff; 179 | font-size: 12px; 180 | } 181 | 182 | @media (prefers-color-scheme: dark) { 183 | .modal-content { 184 | background-color: #2d2d2d; 185 | } 186 | } 187 | 188 | .modal-content .icon { 189 | font-size: 26px !important; 190 | margin: 0; 191 | } 192 | 193 | .help { 194 | width: 100%; 195 | text-align: left; 196 | } 197 | 198 | .help .icon { 199 | font-size: 16px !important; 200 | margin: 0; 201 | } 202 | 203 | .help table { 204 | border-collapse: collapse; 205 | } 206 | 207 | .help td { 208 | border: 1px solid #b1b1b3; 209 | padding: 4px; 210 | } 211 | 212 | .help ul { 213 | margin: 0; 214 | padding-left: 12px; 215 | } 216 | 217 | /********************************************************************************************************************** 218 | * TOOLTIP 219 | **********************************************************************************************************************/ 220 | .tooltip-container { 221 | position: relative; 222 | display: inline-block; 223 | } 224 | 225 | .tooltip-container .tooltip-text { 226 | position: absolute; 227 | z-index: 100; 228 | top: -22px; 229 | left: 10px; 230 | visibility: hidden; 231 | opacity: 0; 232 | height: 22px; 233 | padding: 0 11px; 234 | border-radius: 11px; 235 | background-color: #4e4947; 236 | font-size: 10px; 237 | color: #fff; 238 | text-align: center; 239 | line-height: 22px; 240 | transition: opacity 1s; 241 | } 242 | 243 | .tooltip-container:hover .tooltip-text { 244 | visibility: visible; 245 | opacity: .8; 246 | } 247 | 248 | /********************************************************************************************************************** 249 | * MESSAGES 250 | **********************************************************************************************************************/ 251 | #messages { 252 | display: table; 253 | overflow: hidden; 254 | border-collapse: collapse; 255 | width: 100%; 256 | margin-top: 130px; 257 | } 258 | 259 | .message-container { 260 | display: table-row; 261 | height: 40px; 262 | width: 100%; 263 | white-space: nowrap; 264 | border-bottom: 1px solid #b1b1b3; 265 | } 266 | 267 | .message-container:first-of-type { 268 | height: 20px; 269 | border: 0; 270 | } 271 | 272 | .message { 273 | display: table-cell; 274 | vertical-align: middle; 275 | padding: 0 20px; 276 | } -------------------------------------------------------------------------------- /sidebar/checkmarks-sidebar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | play_circle_outline 17 | 18 | 19 | help_outline 20 | settings 21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | Total: 29 | Ignored: 30 | Problems: 31 |
32 |
Filters:
33 |
34 |
35 |
36 |
37 | 38 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /sidebar/checkmarks-sidebar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Class for the sidebar action. 3 | * @constructor 4 | */ 5 | function CheckmarksSidebar() { 6 | 7 | // Default options for convenient access. 8 | let TIMEOUT = CM_DEFAULTS.getTimeout() * 1000; 9 | let TIMEOUT_OVERRULE = CM_DEFAULTS.getTimeoutOverrule(); 10 | let MAX_TABS = CM_DEFAULTS.getMaxTabs(); 11 | let IGNORED_DIRS = CM_DEFAULTS.getIgnoredDirs(); 12 | let IGNORED_DIRS_ACTIVE = CM_DEFAULTS.getIgnoredDirsActive(); 13 | let INCLUDED_DIRS = CM_DEFAULTS.getIncludedDirs(); 14 | let INCLUDED_DIRS_ACTIVE = CM_DEFAULTS.getIncludedDirsActive(); 15 | let IGNORED_URLS = CM_DEFAULTS.getIgnoredUrls(); 16 | let IGNORED_URLS_ACTIVE = CM_DEFAULTS.getIgnoredUrlsActive(); 17 | let SHOW_FAVICONS = CM_DEFAULTS.getShowFavicons(); 18 | let TO_LOWERCASE = CM_DEFAULTS.getToLowercase(); 19 | let DO_SORT = CM_DEFAULTS.doSort(); 20 | let SORT_UNFILED_BY_DATE = CM_DEFAULTS.sortUnfiledByDate(); 21 | let CLEAR_CACHE = CM_DEFAULTS.getClearCache(); 22 | 23 | // Constants for error-mapping and tooltips. 24 | // Custom error definitions: 25 | const ERROR_DUPLICATE = 'duplicate'; 26 | const ERROR_TIMEOUT = 'timeout'; 27 | const ERROR_UNSPECIFIED = 'unspecified_40x/50x'; // Not yet... 28 | // Request errors: 29 | const ERROR_RESOURCE_NOT_FOUND = 'resource_not_found'; 30 | const ERROR_AUTH_REQUIRED = 'authentication_required'; 31 | // Navigation errors: 32 | const ERROR_SERVER_NOT_FOUND = 'server_not_found'; 33 | const ERROR_CONNECTION_REFUSED = 'connection_refused'; 34 | const ERROR_INSECURE = 'insecure'; 35 | const ERROR_BINDING_ABORTED = 'redirect'; // Actually a warning. Loading will continue. 36 | const ERROR_ABORTED = 'aborted'; 37 | const ERROR_INTERRUPT = 'connection_interrupted'; 38 | 39 | // Error codes provided by the web-extensions API and some HTTP error codes. 40 | const ERROR_CODES_TO_TYPE = { 41 | '2152398878': ERROR_SERVER_NOT_FOUND, 42 | '2152398861': ERROR_CONNECTION_REFUSED, 43 | '2153394164': ERROR_INSECURE, 44 | '2152398850': ERROR_BINDING_ABORTED, 45 | '2147500036': ERROR_ABORTED, 46 | '2152398919': ERROR_INTERRUPT, 47 | '401': ERROR_AUTH_REQUIRED, 48 | '403': ERROR_AUTH_REQUIRED, 49 | '404': ERROR_RESOURCE_NOT_FOUND, 50 | '405': ERROR_AUTH_REQUIRED, 51 | '407': ERROR_AUTH_REQUIRED 52 | }; 53 | 54 | // Maps known error constants to material-icon-ids. 55 | const ERROR_TYPE_TO_ICON = {}; 56 | // Custom error definitions: 57 | ERROR_TYPE_TO_ICON[ERROR_DUPLICATE] = 'content_copy'; 58 | ERROR_TYPE_TO_ICON[ERROR_TIMEOUT] = 'hourglass_full'; 59 | ERROR_TYPE_TO_ICON[ERROR_UNSPECIFIED] = 'warning'; 60 | // Request errors: 61 | ERROR_TYPE_TO_ICON[ERROR_RESOURCE_NOT_FOUND] = 'cloud_off'; 62 | ERROR_TYPE_TO_ICON[ERROR_AUTH_REQUIRED] = 'security'; 63 | // Navigation errors: 64 | ERROR_TYPE_TO_ICON[ERROR_SERVER_NOT_FOUND] = 'dns'; 65 | ERROR_TYPE_TO_ICON[ERROR_CONNECTION_REFUSED] = 'block'; 66 | ERROR_TYPE_TO_ICON[ERROR_INSECURE] = 'no_encryption'; 67 | ERROR_TYPE_TO_ICON[ERROR_BINDING_ABORTED] = 'directions'; 68 | ERROR_TYPE_TO_ICON[ERROR_ABORTED] = 'report'; 69 | ERROR_TYPE_TO_ICON[ERROR_INTERRUPT] = 'report'; 70 | 71 | // DOM objects. 72 | const START = document.getElementById('start'); 73 | const PAUSE = document.getElementById('pause'); 74 | const CANCEL = document.getElementById('cancel'); 75 | const OPTIONS = document.getElementById('options'); 76 | const HELP = document.getElementById('help'); 77 | const APOSTROPHE = document.getElementById('apostrophe'); 78 | const PROGRESS = document.getElementById('progress'); 79 | const PROGRESS_BAR = document.getElementById('progress-bar'); 80 | const STATISTICS = document.getElementById('statistics'); 81 | const STATISTICS_TOTAL = document.getElementById('statistics-total'); 82 | const STATISTICS_IGNORED = document.getElementById('statistics-ignored'); 83 | const STATISTICS_ERRORS = document.getElementById('statistics-errors'); 84 | const FAVICONS = document.getElementById('favicons'); 85 | const FILTERS = document.getElementById('filters'); 86 | const MESSAGES = document.getElementById('messages'); 87 | const MODAL = document.getElementById('modal'); 88 | const MODAL_CANCEL = document.getElementById('modal-cancel'); 89 | const MODAL_CANCEL_CONFIRM = document.getElementById('modal-cancel-confirm'); 90 | const MODAL_CANCEL_CANCEL = document.getElementById('modal-cancel-cancel'); 91 | const MODAL_HELP = document.getElementById('modal-help'); 92 | const MODAL_DELETE = document.getElementById('modal-delete'); 93 | const MODAL_DELETE_CONFIRM = document.getElementById('modal-delete-confirm'); 94 | const MODAL_DELETE_CANCEL = document.getElementById('modal-delete-cancel'); 95 | 96 | const POST_LOAD_TIMEOUT = 2000; 97 | const MIN_WINDOW_WIDTH = 220; // ...to display duration/progress % 98 | const MESSAGES_MARGIN_TOP = '130px'; 99 | const MESSAGES_MARGIN_TOP_FAVICON_BAR = '170px'; 100 | 101 | let startTime; 102 | let hostWindowId; 103 | let modalBookmarkId; 104 | let removalConfirmed = false; 105 | 106 | let filters = []; 107 | 108 | let errors = []; 109 | let bookmarks = []; 110 | let folders = {}; 111 | let bookmarksIgnored = []; 112 | let bookmarksProcessed = {}; 113 | let bookmarksTotal = 0; 114 | let paused = false; 115 | let restored = false; 116 | 117 | let urls = {}; 118 | let tabCount = 0; 119 | let tabRegistry = {}; // tabId => bookmark, tabIdComplete => true/false 120 | let tabRequestMap = {}; // tabId => requestCount, tabUrl => error 121 | let timeoutIds = []; 122 | 123 | /** 124 | * Registers the event-listeners for sidebar controls and sets the icon according to the system theme. 125 | */ 126 | this.init = function () { 127 | console.debug('DEBUG: Initializing...'); 128 | setIcon(); 129 | setFilters(); 130 | restoreRun(); 131 | // Start button that initializes the chain of events. 132 | START.addEventListener('click', start); 133 | START.addEventListener('mouseenter', () => APOSTROPHE.innerText = 'Start/Resume'); 134 | START.addEventListener('mouseleave', () => APOSTROPHE.innerText = ''); 135 | 136 | // Pause button that pauses the chain of events. 137 | PAUSE.addEventListener('click', pause); 138 | PAUSE.addEventListener('mouseenter', () => APOSTROPHE.innerText = 'Pause run'); 139 | PAUSE.addEventListener('mouseleave', () => APOSTROPHE.innerText = ''); 140 | 141 | // Cancel button that interrupts the chain of events. 142 | CANCEL.addEventListener('click', () => { 143 | MODAL.style.display = 'block'; 144 | MODAL_CANCEL.style.display = 'block'; 145 | }); 146 | CANCEL.addEventListener('mouseenter', () => APOSTROPHE.innerText = 'Cancel current or discard paused run'); 147 | CANCEL.addEventListener('mouseleave', () => APOSTROPHE.innerText = ''); 148 | 149 | // Confirmation button for canceling a run. 150 | MODAL_CANCEL_CONFIRM.addEventListener('click', () => { 151 | PAUSE.style.display = 'none'; 152 | CANCEL.style.display = 'none'; 153 | MODAL.style.display = 'none'; 154 | MODAL_CANCEL.style.display = 'none'; 155 | paused = false; 156 | cancel(); 157 | reset(); 158 | }); 159 | 160 | // Hides warning modal without confirmation. 161 | MODAL_CANCEL_CANCEL.addEventListener('click', () => { 162 | MODAL.style.display = 'none'; 163 | MODAL_CANCEL.style.display = 'none'; 164 | }); 165 | 166 | // Open help modal. 167 | HELP.addEventListener('click', () => { 168 | MODAL.style.display = 'block'; 169 | MODAL_HELP.style.display = 'block'; 170 | }); 171 | HELP.addEventListener('mouseenter', () => APOSTROPHE.innerText = 'Show help'); 172 | HELP.addEventListener('mouseleave', () => APOSTROPHE.innerText = ''); 173 | 174 | // Close help modal. 175 | MODAL_HELP.addEventListener('click', () => { 176 | MODAL.style.display = 'none'; 177 | MODAL_HELP.style.display = 'none'; 178 | }); 179 | 180 | // Link to options page. 181 | OPTIONS.addEventListener('click', () => { 182 | browser.runtime.openOptionsPage() 183 | .then(() => console.info('INFO: Options page opened.')); 184 | }); 185 | OPTIONS.addEventListener('mouseenter', () => APOSTROPHE.innerText = 'Open preferences'); 186 | OPTIONS.addEventListener('mouseleave', () => APOSTROPHE.innerText = ''); 187 | 188 | // Confirmation button for the removal of bookmarks. 189 | MODAL_DELETE_CONFIRM.addEventListener('click', () => { 190 | removeBookmark(modalBookmarkId, true); 191 | MODAL.style.display = 'none'; 192 | MODAL_DELETE.style.display = 'none'; 193 | removalConfirmed = true; 194 | }); 195 | 196 | // Hides warning modal without confirmation. 197 | MODAL_DELETE_CANCEL.addEventListener('click', () => { 198 | MODAL.style.display = 'none'; 199 | MODAL_DELETE.style.display = 'none'; 200 | }); 201 | 202 | // Hides modals when the user clicks somewhere else. 203 | window.addEventListener('click', (event) => { 204 | if (event.target === MODAL) { 205 | MODAL.style.display = 'none'; 206 | MODAL_CANCEL.style.display = 'none'; 207 | MODAL_HELP.style.display = 'none'; 208 | MODAL_DELETE.style.display = 'none'; 209 | } 210 | }); 211 | 212 | // Avoid that progress/duration information overlaps controls. 213 | window.addEventListener('resize', () => { 214 | if (window.innerWidth < MIN_WINDOW_WIDTH) { 215 | PROGRESS.innerText = ''; 216 | } 217 | }) 218 | }; 219 | 220 | let setIcon = function () { 221 | const isDarkScheme = window.matchMedia('(prefers-color-scheme: dark)').matches; 222 | console.info(`INFO: Dark color scheme: ${isDarkScheme}.`); 223 | 224 | browser.sidebarAction.setIcon({ 225 | path: isDarkScheme ? { 226 | 16: '/assets/images/icon-light.svg', 227 | 32: '/assets/images/icon-light.svg' 228 | } : { 229 | 16: '/assets/images/icon-dark.svg', 230 | 32: '/assets/images/icon-dark.svg' 231 | } 232 | }).then(() => console.info('INFO: Icon set.')); 233 | }; 234 | 235 | /** 236 | * Restores options from local storage or sets default values. 237 | * @param options {{}} Options map from local storage. 238 | */ 239 | let setOptions = function (options) { 240 | TIMEOUT = typeof options.requestTimeout !== 'undefined' ? options.requestTimeout * 1000 : TIMEOUT; 241 | TIMEOUT_OVERRULE = typeof options.timeoutOverrule !== 'undefined' ? options.timeoutOverrule : TIMEOUT_OVERRULE; 242 | MAX_TABS = typeof options.maxTabs !== 'undefined' ? options.maxTabs : MAX_TABS; 243 | IGNORED_DIRS = typeof options.ignoredDirs !== 'undefined' ? 244 | getOptionsArray(options.ignoredDirs) : IGNORED_DIRS; 245 | IGNORED_DIRS_ACTIVE = typeof options.ignoredDirsActive !== 'undefined' ? 246 | options.ignoredDirsActive : IGNORED_DIRS_ACTIVE; 247 | INCLUDED_DIRS = typeof options.includedDirs !== 'undefined' ? 248 | getOptionsArray(options.includedDirs) : INCLUDED_DIRS; 249 | INCLUDED_DIRS_ACTIVE = typeof options.includedDirsActive !== 'undefined' ? 250 | options.includedDirsActive : INCLUDED_DIRS_ACTIVE; 251 | IGNORED_URLS = typeof options.ignoredUrls !== 'undefined' ? 252 | getOptionsArray(options.ignoredUrls) : IGNORED_URLS; 253 | IGNORED_URLS_ACTIVE = typeof options.ignoredUrlsActive !== 'undefined' ? 254 | options.ignoredUrlsActive : IGNORED_URLS_ACTIVE; 255 | SHOW_FAVICONS = typeof options.showFavicons !== 'undefined' ? options.showFavicons : SHOW_FAVICONS; 256 | TO_LOWERCASE = typeof options.toLowercase !== 'undefined' ? options.toLowercase : TO_LOWERCASE; 257 | DO_SORT = typeof options.doSort !== 'undefined' ? options.doSort : DO_SORT; 258 | SORT_UNFILED_BY_DATE = typeof options.sortUnfiledByDate !== 'undefined' ? 259 | options.sortUnfiledByDate : SORT_UNFILED_BY_DATE; 260 | CLEAR_CACHE = typeof options.clearCache !== 'undefined' ? options.clearCache : CLEAR_CACHE; 261 | }; 262 | 263 | /** 264 | * Transforms a comma separated string into an array of options. 265 | * @param option {string} Raw comma separated string. 266 | * @returns {string[]} Array of options. 267 | */ 268 | let getOptionsArray = function (option) { 269 | return option 270 | .split(',') 271 | .map(x => x.trim()) 272 | .filter(x => x !== ''); 273 | }; 274 | 275 | /** 276 | * Activates or deactivates filters for the given errors and sets button style accordingly. 277 | * @param button {HTMLElement} Button that toggles the filter. 278 | * @param error {boolean} Type of error that should be filtered or not. 279 | */ 280 | let toggleFilter = function (button, error) { 281 | if (button.classList.contains('error')) { 282 | button.classList.remove('error'); 283 | filters.push(error); 284 | } else { 285 | button.classList.add('error'); 286 | filters = filters.filter((filter) => filter !== error); 287 | } 288 | } 289 | 290 | /** 291 | * Creates the UI for filtering errors. 292 | */ 293 | let setFilters = function () { 294 | const seen = []; 295 | const filterButtons = []; 296 | 297 | const apostrophe = document.createTextNode(''); 298 | 299 | Object.entries(ERROR_TYPE_TO_ICON).forEach((entry) => { 300 | const errorText = entry[0].replace(/_/g, ' '); 301 | const errorIcon = entry[1]; 302 | 303 | if (!seen.includes(errorIcon)) { 304 | seen.push(errorIcon); 305 | const filterButton = createIcon(errorIcon, 'error', ''); 306 | filterButton.classList.add('button'); 307 | 308 | filterButton.addEventListener('click', () => { 309 | toggleFilter(filterButton, errorIcon); 310 | createList(errors, true); 311 | }); 312 | 313 | filterButton.addEventListener('mouseenter', () => { 314 | apostrophe.textContent = ` | ${errorText}`; 315 | }); 316 | 317 | filterButton.addEventListener('mouseleave', () => { 318 | apostrophe.textContent = ''; 319 | }); 320 | 321 | FILTERS.append(filterButton); 322 | filterButtons.push(filterButton); 323 | } 324 | }); 325 | 326 | const toggleButton = document.createElement('a'); 327 | toggleButton.append(document.createTextNode('| toggle')); 328 | toggleButton.classList.add('button'); 329 | toggleButton.addEventListener('click', () => { 330 | filterButtons.forEach((button) => { 331 | toggleFilter(button, button.innerText); 332 | createList(errors, true); 333 | }); 334 | }); 335 | FILTERS.append(toggleButton); 336 | FILTERS.append(apostrophe); 337 | }; 338 | 339 | /** 340 | * Resets almost all state members to their initial state. 341 | */ 342 | let resetState = function () { 343 | paused = false; 344 | restored = false; 345 | folders = {}; 346 | urls = {}; 347 | tabCount = 0; 348 | tabRegistry = {}; 349 | tabRequestMap = {}; 350 | timeoutIds = []; 351 | }; 352 | 353 | /** 354 | * Resets bookmark information. 355 | */ 356 | let resetBookmarks = function () { 357 | bookmarks = []; 358 | bookmarksIgnored = []; 359 | folders = {}; 360 | // Keep information for restored or resumed runs! 361 | if (!paused && !restored) { 362 | errors = []; 363 | bookmarksProcessed = {}; 364 | } 365 | browser.storage.local.remove(['processed', 'errors']) 366 | .then(() => console.info('INFO: Removed run from local storage.')); 367 | }; 368 | 369 | /** 370 | * Resets UI elements. 371 | */ 372 | let resetUI = function () { 373 | PROGRESS_BAR.style.width = '0%'; 374 | PROGRESS.innerText = ''; 375 | MESSAGES.innerHTML = ''; 376 | STATISTICS.style.display = 'none'; 377 | FAVICONS.style.display = 'none'; 378 | FAVICONS.innerHTML = ''; 379 | FILTERS.style.display = 'none'; 380 | MESSAGES.style.marginTop = MESSAGES_MARGIN_TOP; 381 | }; 382 | 383 | /** 384 | * Resets everything. 385 | */ 386 | let reset = function () { 387 | resetState(); 388 | resetBookmarks(); 389 | resetUI(); 390 | }; 391 | 392 | /** 393 | * Registers listeners that keep the chain of events going. 394 | */ 395 | let registerListeners = function () { 396 | browser.tabs.onRemoved.addListener(onRemoved); 397 | browser.webNavigation.onCompleted.addListener(onNavigationCompleted); 398 | browser.webNavigation.onErrorOccurred.addListener(onNavigationError); 399 | browser.webRequest.onCompleted.addListener( 400 | onRequestCompleted, 401 | {urls: ['']} 402 | ); 403 | browser.webRequest.onAuthRequired.addListener( 404 | cancelAuth, 405 | {urls: ['']}, 406 | ["blocking"] 407 | ); 408 | }; 409 | 410 | /** 411 | * Removes listeners again. 412 | */ 413 | let removeListeners = function () { 414 | browser.tabs.onRemoved.removeListener(onRemoved); 415 | browser.webNavigation.onCompleted.removeListener(onNavigationCompleted); 416 | browser.webNavigation.onErrorOccurred.removeListener(onNavigationError); 417 | browser.webRequest.onCompleted.removeListener(onRequestCompleted); 418 | browser.webRequest.onAuthRequired.removeListener(cancelAuth); 419 | }; 420 | 421 | /** 422 | * Initializes the chain of events. 423 | */ 424 | let start = function () { 425 | startTime = Date.now(); 426 | 427 | PAUSE.style.display = 'inline'; 428 | CANCEL.style.display = 'inline'; 429 | 430 | browser.storage.local.get() 431 | .then((storage) => { 432 | setOptions(storage.options || {}); 433 | const processedCount = Object.keys(bookmarksProcessed).length; 434 | if (processedCount > 0 && processedCount !== bookmarksTotal) { 435 | console.debug('DEBUG: Resuming run...'); 436 | 437 | resetBookmarks(); 438 | resetState(); 439 | } else { 440 | console.debug('DEBUG: New run...'); 441 | if (processedCount > 0) { 442 | reset(); 443 | } 444 | createPlaceholder(); 445 | } 446 | 447 | if (CLEAR_CACHE) { 448 | browser.browsingData.removeCache({}) 449 | .then(() => { 450 | console.info('INFO: Cleared cache.'); 451 | browser.bookmarks.getTree() 452 | .then(run); 453 | }); 454 | } else { 455 | browser.bookmarks.getTree() 456 | .then(run); 457 | } 458 | }); 459 | } 460 | 461 | /** 462 | * Pauses a running bookmarks-check. Events are blocked, listeners and timeouts removed, opened tabs closed 463 | * and errors are stored before state is reset. 464 | */ 465 | let pause = function () { 466 | paused = true; 467 | PAUSE.style.display = 'none'; 468 | cancel(); 469 | storeRun(); 470 | }; 471 | 472 | /** 473 | * Cancels a running bookmarks-check. Events are blocked, listeners and timeouts removed and opened tabs closed. 474 | */ 475 | let cancel = function () { 476 | removeListeners(); 477 | 478 | timeoutIds.forEach((id) => { 479 | clearTimeout(id); 480 | }); 481 | 482 | Object.keys(tabRegistry).forEach((id) => { 483 | if (!id.endsWith('Complete')) { 484 | browser.tabs.remove(parseInt(id)) 485 | .then(() => delete tabRegistry[id]); 486 | } 487 | }); 488 | }; 489 | 490 | /** 491 | * Finishing touches after all bookmarks are processed; 492 | */ 493 | let finish = function () { 494 | console.debug('DEBUG: Finishing...'); 495 | storeRun(); 496 | removeListeners(); 497 | resetState(); 498 | 499 | PAUSE.style.display = 'none'; 500 | if (SHOW_FAVICONS) { 501 | FAVICONS.style.display = 'none'; 502 | MESSAGES.style.marginTop = MESSAGES_MARGIN_TOP; 503 | } 504 | 505 | let endTime = Date.now(); 506 | if (window.innerWidth > MIN_WINDOW_WIDTH && endTime - startTime < 1000 * 60 * 60 * 24) { 507 | // If the sidebar is to narrow the duration would overlap the controls. 508 | // This simple ms-conversion only works for durations shorter than one day. 509 | setTimeout(() => { 510 | PROGRESS.innerText = new Date(endTime - startTime) 511 | .toUTCString() 512 | .slice(17, 25); 513 | }, POST_LOAD_TIMEOUT); 514 | } 515 | }; 516 | 517 | /** 518 | * Cancels request with authentication. 519 | * @param details {{}} 520 | * @return {{cancel: boolean}} 521 | */ 522 | let cancelAuth = function (details) { 523 | console.info(`WARN: Canceling authentication for request: ${details.url};`); 524 | tabRequestMap[details.url] = ERROR_AUTH_REQUIRED; 525 | 526 | return {cancel: true}; 527 | }; 528 | 529 | /** 530 | * Initializes the chain of events by collecting bookmark-information and loading the first batch of bookmarks. 531 | * The batch-size is dependent on the configured maximum number of tabs. 532 | * @param tree {browser.bookmarks.BookmarkTreeNode} 533 | */ 534 | let run = function (tree) { 535 | bookmarksTotal = 0; 536 | folders = {}; 537 | // Collect bookmark-information 538 | walk(tree[0], '/'); 539 | 540 | if (DO_SORT) { 541 | console.info(`INFO: Sorting... Unfiled by date: ${SORT_UNFILED_BY_DATE}`); 542 | sort(); 543 | } 544 | 545 | FILTERS.style.display = 'block'; 546 | setStatistics(); 547 | setProgress(); 548 | registerListeners(); 549 | 550 | browser.windows.getCurrent() 551 | .then((window) => { 552 | // Open tabs only in the window the extension was started i n! 553 | hostWindowId = window.id; 554 | 555 | while (tabCount < MAX_TABS && bookmarks.length > 0) { 556 | loadBookmark(); 557 | } 558 | }, (error) => { 559 | console.error(`ERROR: Could not get window id: ${error};`); 560 | }); 561 | }; 562 | 563 | /** 564 | * Recursively traverses bookmark-folders. 565 | * @param treeItem {browser.bookmarks.BookmarkTreeNode} 566 | * @param path {string} Path to the currently processed bookmark-folder. 567 | */ 568 | let walk = function (treeItem, path) { 569 | if (DO_SORT && treeItem.parentId && treeItem.parentId !== 'root________' && (treeItem.url || treeItem.children)) { 570 | if (!(treeItem.parentId in folders)) { 571 | folders[treeItem.parentId] = [treeItem]; 572 | } else { 573 | folders[treeItem.parentId].push(treeItem); 574 | } 575 | } 576 | 577 | if (treeItem.url && treeItem.url.startsWith('http')) { 578 | bookmarksTotal++; 579 | if ((IGNORED_URLS_ACTIVE && isIgnored(treeItem.url, IGNORED_URLS)) || 580 | (IGNORED_DIRS_ACTIVE && isIgnored(path, IGNORED_DIRS)) || 581 | (INCLUDED_DIRS_ACTIVE && !isIgnored(path, INCLUDED_DIRS))) { 582 | // The bookmark or currently processed folder is ignored. 583 | bookmarksIgnored.push(treeItem.id); 584 | } else if (!(treeItem.id in bookmarksProcessed)) { 585 | // Format 586 | if (TO_LOWERCASE) { 587 | browser.bookmarks.update(treeItem.id, {title: treeItem.title.toLowerCase()}); 588 | } 589 | 590 | treeItem['path'] = path.replace(/(^\/\/|\/$)/g, '').toLowerCase(); 591 | 592 | if (treeItem.url in urls) { 593 | // Bookmark is duplicated. 594 | reportError(treeItem, ERROR_DUPLICATE); 595 | bookmarksProcessed[treeItem.id] = true; 596 | } else { 597 | // Store urls for duplicate-check. 598 | urls[treeItem.url] = true; 599 | bookmarks.push(treeItem); 600 | } 601 | } 602 | } else if (treeItem.children) { 603 | // In order to get statistics recurse into the folder even if it is ignored! 604 | treeItem.children.forEach((child) => { 605 | walk(child, path + treeItem.title + '/'); 606 | }); 607 | } 608 | }; 609 | 610 | /** 611 | * Sort function that sorts alphanumeric and folders before bookmarks. Unfiled (other) bookmarks are sorted by date. 612 | * @param a {browser.bookmarks.BookmarkTreeNode} Node for sorting. 613 | * @param b {browser.bookmarks.BookmarkTreeNode} Node for sorting. 614 | * @returns {number} Result for sorting. 615 | */ 616 | let sortCompare = function (a, b) { 617 | let result = 0; 618 | if (a.children && !b.children) { 619 | result = -1; 620 | } else if (!a.children && b.children) { 621 | result = 1; 622 | } else if (SORT_UNFILED_BY_DATE && a.parentId === 'unfiled_____') { 623 | result = a.dateAdded - b.dateAdded; 624 | } 625 | 626 | return result === 0 ? a.title.localeCompare(b.title) : result; 627 | } 628 | 629 | /** 630 | * Sorts bookmarks by title. 631 | */ 632 | let sort = function () { 633 | Object.entries(folders).forEach((entry) => { 634 | entry[1].sort(sortCompare).forEach((bookmark, index) => { 635 | browser.bookmarks.move(bookmark.id, {parentId: bookmark.parentId, index: index}); 636 | }) 637 | }); 638 | 639 | }; 640 | 641 | /** 642 | * Checks if a bookmark should be ignored. Its called for checking folder and url separately. 643 | * @param pathOrUrl {string} Path to the bookmark or its url. 644 | * @param ignoreArray {string[]} Array of ignored (partial) paths or urls. 645 | * @returns {boolean} 646 | */ 647 | let isIgnored = function (pathOrUrl, ignoreArray) { 648 | return pathOrUrl !== '' && ignoreArray.some((x) => { 649 | return pathOrUrl.toLowerCase().includes(x.toLowerCase()); 650 | }); 651 | }; 652 | 653 | /** 654 | * Creates a new tab and loads the next bookmark from the stack. It also sets the procedure which is called on 655 | * timeout. 656 | */ 657 | let loadBookmark = function () { 658 | tabCount++; 659 | let bookmark = bookmarks.shift(); 660 | 661 | browser.tabs.create({url: bookmark.url, windowId: hostWindowId, active: false}) 662 | .then((tab) => { 663 | tabRegistry[tab.id] = bookmark; 664 | 665 | timeoutIds.push(setTimeout(() => { 666 | onTimeout(tab); 667 | }, TIMEOUT)); 668 | }, (error) => { 669 | tabCount--; 670 | console.error(`ERROR: Could not create tab: ${error};`); 671 | }); 672 | }; 673 | 674 | /** 675 | * Identifies several common HTTP errors and counts successful sub-requests while loading a bookmark. 676 | * @param details {{}} Details from {browser.webRequest.onCompleted}. 677 | */ 678 | let onRequestCompleted = function (details) { 679 | if (details.tabId in tabRegistry || details.tabId === -1) { 680 | if (details.type === 'main_frame' && details.statusCode >= 400) { 681 | if (details.statusCode in ERROR_CODES_TO_TYPE) { 682 | console.warn(`WARN: HTTP error: ${details.statusCode} tab: ${details.tabId} url: ${details.url}`); 683 | handleRequestError(details, ERROR_CODES_TO_TYPE[details.statusCode]); 684 | } else { 685 | // >= 500 or some other 40x... 686 | console.error(`ERROR: HTTP error: ${details.statusCode} tab: ${details.tabId} url: ${details.url}`); 687 | handleRequestError(details, ERROR_UNSPECIFIED); 688 | } 689 | } else if (details.statusCode >= 200 && details.statusCode < 300) { 690 | // Count successful requests while loading! 691 | if (details.tabId in tabRequestMap) { 692 | tabRequestMap[details.tabId] += 1; 693 | } else { 694 | tabRequestMap[details.tabId] = 1; 695 | } 696 | } 697 | } 698 | }; 699 | 700 | /** 701 | * Checks if a bookmark is loaded completely or has produced errors and dispatches to success- and error-handlers. 702 | * @param details {{}} Details from {browser.webRequest.onCompleted}. 703 | */ 704 | let onNavigationCompleted = function (details) { 705 | if (details.tabId in tabRegistry) { 706 | if (details.url in tabRequestMap) { 707 | // HTTP error detected in onRequestCompleted but tabId was -1. 708 | handleError(details.tabId, tabRequestMap[details.url]); 709 | } else { 710 | browser.tabs.get(details.tabId) 711 | .then((tab) => { 712 | const tabCompleteKey = tab.id + 'Complete'; 713 | if (tab.status === 'complete' && !(tabCompleteKey in tabRegistry)) { 714 | // Sometimes a tab reports "status complete" twice? 715 | tabRegistry[tabCompleteKey] = true; 716 | handleSuccess(tab); 717 | } 718 | }); 719 | } 720 | } 721 | }; 722 | 723 | /** 724 | * Identifies several known navigation errors. 725 | * @param details {{}} Details from {browser.webRequest.onNavigationError}. 726 | */ 727 | let onNavigationError = function (details) { 728 | if (details.tabId in tabRegistry) { 729 | const errorCode = details.error.replace('Error code ', ''); 730 | 731 | if (errorCode in ERROR_CODES_TO_TYPE) { 732 | let errorType = ERROR_CODES_TO_TYPE[errorCode]; 733 | // override if canceled by extension (for example cancelAuth). 734 | if (details.url in tabRequestMap) { 735 | errorType = tabRequestMap[details.url]; 736 | } 737 | // Capitalize and remove underline characters! 738 | let errorString = (errorType.charAt(0).toUpperCase() + errorType.slice(1)).replace(/_/g, ' '); 739 | let bookmark = tabRegistry[details.tabId]; 740 | 741 | if (details.frameId === 0) { 742 | // Main frame... 743 | handleError(details.tabId, errorType); 744 | console.warn(`WARN: ${errorString}; bookmark: ${bookmark.title}; request: ${details.url};`); 745 | } else { 746 | // Sub-requests are more likely to be aborted by content-blockers. 747 | errorString = errorString.replace('Redirect', 'Aborted/redirected'); 748 | console.info(`INFO: ${errorString}; bookmark: ${bookmark.title}; request: ${details.url};`); 749 | } 750 | } else { 751 | // Unknown 752 | console.error(`ERROR: Unknown navigation error: ${JSON.stringify(details)};`); 753 | } 754 | } 755 | }; 756 | 757 | /** 758 | * After a tab is removed, ie the bookmark was loaded successfully or not, statistics and state are updated. 759 | * @param tabId {number} Id of the removed tab. 760 | */ 761 | let onRemoved = function (tabId) { 762 | if (!paused && tabId in tabRegistry) { 763 | bookmarksProcessed[tabRegistry[tabId].id] = true; 764 | delete tabRegistry[tabId]; 765 | tabCount--; 766 | 767 | setProgress(); 768 | 769 | if (bookmarks.length + tabCount === 0) { 770 | finish(); 771 | } else if (bookmarks.length > 0) { 772 | loadBookmark(); 773 | } 774 | } 775 | }; 776 | 777 | /** 778 | * Removes a tab on timeout and decides if the bookmark is considered to be broken, based on successful requests. 779 | * @param tab {browser.tabs.Tab} that timed out. 780 | */ 781 | let onTimeout = function (tab) { 782 | const tabCompleteKey = tab.id + 'Complete'; 783 | if (tab.id in tabRegistry && !(tabCompleteKey in tabRegistry)) { 784 | if (!(tab.id in tabRequestMap && tabRequestMap[tab.id] > TIMEOUT_OVERRULE)) { 785 | handleError(tab.id, ERROR_TIMEOUT); 786 | } else { 787 | tabRegistry[tabCompleteKey] = false; 788 | let bookmark = tabRegistry[tab.id]; 789 | let responses = tabRequestMap[tab.id]; 790 | console.info(`INFO: Timeout overruled; ${responses} successful requests; bookmark: ${bookmark.url};`); 791 | handleSuccess(tab); 792 | } 793 | } 794 | }; 795 | 796 | /** 797 | * Removes the successfully loaded tab and displays its favicon - if present. 798 | * @param tab {browser.tabs.Tab} Successfully loaded tab. 799 | */ 800 | let handleSuccess = function (tab) { 801 | if (SHOW_FAVICONS && tab.favIconUrl) { 802 | setFavicon(tab.favIconUrl); 803 | } 804 | timeoutIds.push(setTimeout(() => { 805 | browser.tabs.remove(tab.id); 806 | }, POST_LOAD_TIMEOUT)); 807 | }; 808 | 809 | /** 810 | * Initializes the chain of events that is followed by errors while loading a bookmark. This includes reporting and 811 | * removing the tab. On redirection "errors" the tab is not removed. 812 | * @param tabId {number} Id of the tab where the error occurred. 813 | * @param error {string} Type of error. 814 | */ 815 | let handleError = function (tabId, error) { 816 | let unverifiedBookmark = tabRegistry[tabId]; 817 | 818 | if (unverifiedBookmark) { 819 | // Keep on loading on redirect. The tab is likely to complete successfully. 820 | if (error !== ERROR_BINDING_ABORTED) { 821 | browser.tabs.remove(tabId); 822 | } 823 | reportError(unverifiedBookmark, error); 824 | } 825 | }; 826 | 827 | /** 828 | * Handles request errors where the corresponding tab id is known or unknown (-1). 829 | * @param details {{}} Details from {browser.webRequest.onCompleted}. 830 | * @param error {string} Type of error. 831 | */ 832 | let handleRequestError = function (details, error) { 833 | // Url-error-relation might be need by onNavigationCompleted if the tab id is -1. 834 | tabRequestMap[details.url] = error; 835 | if (details.tabId !== -1) { 836 | handleError(details.tabId, error); 837 | } 838 | }; 839 | 840 | /** 841 | * Temporarily stores errors and appends an error message to the messages-list of the UI. 842 | * @param bookmark {browser.bookmarks.BookmarkTreeNode} Invalid bookmark. 843 | * @param error {string} Type of error. 844 | */ 845 | let reportError = function (bookmark, error) { 846 | errors.push({ 847 | bookmark: { 848 | id: bookmark.id, 849 | title: bookmark.title, 850 | url: bookmark.url, 851 | path: bookmark.path 852 | }, 853 | error: error 854 | }); 855 | 856 | if (!filters.includes(ERROR_TYPE_TO_ICON[error])) { 857 | createListEntry(bookmark, error); 858 | } 859 | setStatistics(); 860 | }; 861 | 862 | /** 863 | * Creates an empty error-list row to allow tooltips above the first regular error-entry. 864 | */ 865 | let createPlaceholder = function () { 866 | const placeholder = document.createElement('div'); 867 | placeholder.className = 'message-container'; 868 | MESSAGES.append(placeholder); 869 | }; 870 | 871 | /** 872 | * Creates the error list from existing errors. 873 | * @param existingErrors {[{bookmark: browser.bookmarks.BookmarkTreeNode, error: string}]} Error storage to use 874 | * @param uiOnly {boolean} If set to true the error is not added to the internal list of errors. 875 | * Filters are applied to the 876 | * UI only. 877 | */ 878 | let createList = function (existingErrors, uiOnly) { 879 | MESSAGES.innerHTML = ''; 880 | createPlaceholder(); 881 | 882 | existingErrors.forEach((error) => { 883 | if (!filters.includes(ERROR_TYPE_TO_ICON[error.error])) { 884 | if (uiOnly) { 885 | createListEntry(error.bookmark, error.error); 886 | } else { 887 | reportError(error.bookmark, error.error); 888 | } 889 | } 890 | }); 891 | }; 892 | 893 | /** 894 | * Creates an entry for the given error. 895 | * @param bookmark {browser.bookmarks.BookmarkTreeNode} Invalid bookmark. 896 | * @param error {string} Type of error. 897 | */ 898 | let createListEntry = function (bookmark, error) { 899 | const messageContainer = document.createElement('div'); 900 | messageContainer.id = bookmark.id; 901 | messageContainer.className = 'message-container'; 902 | 903 | const message = document.createElement('div'); 904 | message.className = 'message'; 905 | message.append(createActionIcons(bookmark)); 906 | message.append(createIcon('folder_open', '', '/' + bookmark.path)); 907 | message.append(createIcon(ERROR_TYPE_TO_ICON[error], 'error', error.replace(/_/g, ' '))); 908 | message.append(document.createTextNode(bookmark.title.toLowerCase())); 909 | 910 | messageContainer.append(message); 911 | 912 | MESSAGES.append(messageContainer); 913 | }; 914 | 915 | /** 916 | * Creates action icons for an invalid bookmark. 917 | * @param bookmark {browser.bookmarks.BookmarkTreeNode} 918 | * @returns {HTMLElement} 919 | */ 920 | let createActionIcons = function (bookmark) { 921 | const actionContainer = document.createElement('span'); 922 | 923 | const deleteFromBookmarksIcon = createIcon('delete', 'button', 'delete bookmark'); 924 | deleteFromBookmarksIcon.addEventListener('click', () => { 925 | if (removalConfirmed) { 926 | removeBookmark(bookmark.id, true); 927 | } else { 928 | modalBookmarkId = bookmark.id; 929 | MODAL.style.display = 'block'; 930 | MODAL_DELETE.style.display = 'block'; 931 | MODAL_HELP.style.display = 'none'; 932 | } 933 | }); 934 | actionContainer.append(deleteFromBookmarksIcon); 935 | 936 | const deleteFromListIcon = createIcon('check_circle', 'button', 'checked; remove from list'); 937 | deleteFromListIcon.addEventListener('click', () => { 938 | removeBookmark(bookmark.id, false); 939 | }); 940 | actionContainer.append(deleteFromListIcon); 941 | 942 | const launchIcon = createIcon('launch', 'button', 'launch in a new tab'); 943 | launchIcon.addEventListener('click', () => { 944 | browser.tabs.create({url: bookmark.url, index: 1}) 945 | .then((tab) => { 946 | const saveIcon = createIcon('save', 'button', 'update with url of launched tab'); 947 | saveIcon.addEventListener('click', () => { 948 | browser.tabs.get(tab.id) 949 | .then((loadedTab) => { 950 | browser.bookmarks.update(bookmark.id, {url: loadedTab.url}) 951 | .then(() => { 952 | setTempClass(saveIcon, 'success'); 953 | console.info(`INFO: Updated ${bookmark.title} to ${loadedTab.url};`); 954 | }, (error) => { 955 | setTempClass(saveIcon, 'error'); 956 | console.error(`ERROR: Could not update ${bookmark.title}: ${error};`); 957 | }); 958 | }); 959 | }); 960 | launchIcon.remove(); 961 | actionContainer.append(saveIcon); 962 | }); 963 | }); 964 | actionContainer.append(launchIcon); 965 | 966 | return actionContainer; 967 | }; 968 | 969 | /** 970 | * Creates tooltip icons for error types or simple icons for actions. 971 | * @param iconId {string} Id of the material icon. 972 | * @param cssClass {string} (Optional) Additional CSS class 973 | * @param tooltip {string} (Optional) Tooltip for the icon. 974 | * @returns {HTMLElement} 975 | */ 976 | let createIcon = function (iconId, cssClass, tooltip) { 977 | let icon; 978 | 979 | const simpleIcon = document.createElement('i'); 980 | simpleIcon.classList = 'material-icons icon'; 981 | simpleIcon.append(document.createTextNode(iconId)); 982 | 983 | if (cssClass && cssClass !== '') { 984 | simpleIcon.classList.add(cssClass); 985 | } 986 | 987 | if (tooltip) { 988 | const tooltipIconContainer = document.createElement('div'); 989 | const tooltipTextContainer = document.createElement('span'); 990 | const tooltipText = document.createTextNode(tooltip); 991 | 992 | tooltipIconContainer.className = 'tooltip-container'; 993 | tooltipTextContainer.className = 'tooltip-text'; 994 | 995 | tooltipTextContainer.append(tooltipText); 996 | 997 | tooltipIconContainer.append(simpleIcon); 998 | tooltipIconContainer.append(tooltipTextContainer); 999 | 1000 | icon = tooltipIconContainer; 1001 | } else { 1002 | icon = simpleIcon; 1003 | } 1004 | 1005 | return icon; 1006 | }; 1007 | 1008 | /** 1009 | * Temporarily sets the given CSS class 1010 | * @param element {HTMLElement} Element where the class should be set. 1011 | * @param cssClass {string} Name of the class to set. 1012 | */ 1013 | let setTempClass = function (element, cssClass) { 1014 | element.classList.add(cssClass); 1015 | 1016 | timeoutIds.push(setTimeout(() => { 1017 | element.classList.remove(cssClass); 1018 | }, 2000)); 1019 | }; 1020 | 1021 | /** 1022 | * Updates progress information. 1023 | */ 1024 | let setProgress = function () { 1025 | const processedCount = Object.keys(bookmarksProcessed).length; 1026 | const percent = Math.round(processedCount / (bookmarksTotal - bookmarksIgnored.length) * 100) + '%'; 1027 | PROGRESS_BAR.style.width = percent; 1028 | if (window.innerWidth > MIN_WINDOW_WIDTH) { 1029 | PROGRESS.innerText = percent; 1030 | } 1031 | }; 1032 | 1033 | /** 1034 | * Displays statistics for scanned bookmarks (total, ignored, problems). 1035 | */ 1036 | let setStatistics = function () { 1037 | STATISTICS.style.display = 'block'; 1038 | STATISTICS_TOTAL.innerText = bookmarksTotal; 1039 | STATISTICS_IGNORED.innerText = bookmarksIgnored.length + ''; 1040 | STATISTICS_ERRORS.innerText = errors.length + ''; 1041 | }; 1042 | 1043 | /** 1044 | * Displays a favicon beyond the progress bar for limited period of time. 1045 | * @param favIconUrl {string} Source of the favicon 1046 | */ 1047 | let setFavicon = function (favIconUrl) { 1048 | // preload 1049 | const image = new Image(); 1050 | image.src = favIconUrl; 1051 | 1052 | const favIcon = document.createElement('img'); 1053 | favIcon.src = image.src; 1054 | favIcon.alt = ''; 1055 | favIcon.className = 'favicon'; 1056 | favIcon.crossOrigin = 'anonymous'; 1057 | FAVICONS.prepend(favIcon); 1058 | FAVICONS.style.display = 'block'; 1059 | MESSAGES.style.marginTop = MESSAGES_MARGIN_TOP_FAVICON_BAR; 1060 | }; 1061 | 1062 | /** 1063 | * Removes a bookmark from the UI list or permanently (from the profile). 1064 | * @param id {string} Id of the bookmark that is to be removed. 1065 | * @param permanently {boolean} Flag to indicate whether the bookmark should be removed permanently. 1066 | */ 1067 | let removeBookmark = function (id, permanently) { 1068 | if (permanently) { 1069 | browser.bookmarks.remove(id) 1070 | .then(() => { 1071 | bookmarksTotal--; 1072 | delete bookmarksProcessed[id]; 1073 | hideBookmark(id); 1074 | }, (error) => { 1075 | // After a reported redirection another error may occur for the same bookmark which results in 1076 | // two entries in the UI. 1077 | console.warn(`WARNING: Could not remove bookmark ${id}: ${error}; Already removed before?`); 1078 | }); 1079 | } else { 1080 | hideBookmark(id); 1081 | } 1082 | }; 1083 | 1084 | /** 1085 | * Hides a bookmark from the UI list and updates statistics. 1086 | * @param id {string} Id of the bookmark that is to be removed. 1087 | */ 1088 | let hideBookmark = function (id) { 1089 | document.getElementById(id).remove(); 1090 | errors = errors.filter((error) => error.bookmark.id !== id); 1091 | setStatistics(); 1092 | }; 1093 | 1094 | /** 1095 | * Stores run bookmarks in local storage. 1096 | */ 1097 | let storeRun = function () { 1098 | browser.storage.local.set({ 1099 | total: bookmarksTotal, 1100 | ignored: bookmarksIgnored, 1101 | processed: bookmarksProcessed, 1102 | errors: errors 1103 | }).then(() => console.info('INFO: Stored run in local storage.')); 1104 | } 1105 | 1106 | /** 1107 | * Restores run from local storage - if available. 1108 | */ 1109 | let restoreRun = function () { 1110 | browser.storage.local.get() 1111 | .then((storage) => { 1112 | if (storage.processed && Object.keys(storage.processed).length > 0) { 1113 | console.debug('DEBUG: Restoring run...'); 1114 | console.debug(storage.processed, storage.errors); 1115 | 1116 | restored = true; 1117 | bookmarksTotal = storage.total; 1118 | bookmarksIgnored = storage.ignored; 1119 | bookmarksProcessed = storage.processed; 1120 | 1121 | CANCEL.style.display = 'inline'; 1122 | STATISTICS.style.display = 'block'; 1123 | FILTERS.style.display = 'block'; 1124 | 1125 | createList(storage.errors, false); 1126 | setProgress(); 1127 | setStatistics(); 1128 | } 1129 | }); 1130 | } 1131 | } 1132 | 1133 | const checkmarksSidebar = new CheckmarksSidebar(); 1134 | checkmarksSidebar.init(); 1135 | --------------------------------------------------------------------------------