diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 1b1e799e..f49ef671 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -11,7 +11,9 @@ body: options: - label: I have searched the existing issues required: true - - label: I have checked the [FAQ](https://github.com/will-stone/browserosaurus/blob/master/docs/faq.md) + - label: + I have checked the + [FAQ](https://github.com/will-stone/browserosaurus/blob/master/docs/faq.md) required: true - type: textarea attributes: @@ -40,13 +42,13 @@ body: attributes: label: Browserosaurus version description: This can be found in Preferences > About - placeholder: "e.g. 15.1.3" + placeholder: 'e.g. 15.1.3' validations: required: true - type: input attributes: label: macOS version - placeholder: "e.g. 11.6" + placeholder: 'e.g. 11.6' validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index a4bf18d3..46f8e1f0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,4 +3,6 @@ blank_issues_enabled: false contact_links: - name: Get help in GitHub Discussions url: https://github.com/will-stone/browserosaurus/discussions - about: Have a question? Not sure if your issue affects everyone reproducibly? The quickest way to get help is on Browserosaurus's GitHub Discussions! + about: + Have a question? Not sure if your issue affects everyone reproducibly? The + quickest way to get help is on Browserosaurus's GitHub Discussions! diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a70e0c45..79720e1c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,9 +12,9 @@ jobs: - name: Install Node.js uses: actions/setup-node@v1 with: - node-version: 15.5.x + node-version: 16.13.x - run: npm install --no-audit - run: npm run lint - run: npm run typecheck - run: npm run test - - run: npm run package \ No newline at end of file + - run: npm run package diff --git a/.ncurc b/.ncurc deleted file mode 100644 index 3a6c1fb9..00000000 --- a/.ncurc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "reject": ["eslint"] -} diff --git a/.vscode/settings.json b/.vscode/settings.json index 64d5826e..34ecd926 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ "sash.hoverBorder": "#2f6197" }, "peacock.color": "#234870", - "typescript.preferences.importModuleSpecifier": "relative" + "typescript.preferences.importModuleSpecifier": "relative", + "cSpell.words": ["fullscreenable", "maximizable", "minimizable"] } diff --git a/LICENSE.md b/LICENSE.md index 70315a61..4ac571c9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,632 @@ -MIT License - -Copyright (c) 2017-present, Will Stone - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. Everyone is +permitted to copy and distribute verbatim copies of this license document, but +changing it is not allowed. + + Preamble + +The GNU General Public License is a free, copyleft license for software and +other kinds of works. + +The licenses for most software and other practical works are designed to take +away your freedom to share and change the works. By contrast, the GNU General +Public License is intended to guarantee your freedom to share and change all +versions of a program--to make sure it remains free software for all its users. +We, the Free Software Foundation, use the GNU General Public License for most of +our software; it applies also to any other work released this way by its +authors. You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the freedom to +distribute copies of free software (and charge for them if you wish), that you +receive source code or can get it if you want it, that you can change the +software or use pieces of it in new free programs, and that you know you can do +these things. + +To protect your rights, we need to prevent others from denying you these rights +or asking you to surrender the rights. Therefore, you have certain +responsibilities if you distribute copies of the software, or if you modify it: +responsibilities to respect the freedom of others. + +For example, if you distribute copies of such a program, whether gratis or for a +fee, you must pass on to the recipients the same freedoms that you received. You +must make sure that they, too, receive or can get the source code. And you must +show them these terms so they know their rights. + +Developers that use the GNU GPL protect your rights with two steps: (1) assert +copyright on the software, and (2) offer you this License giving you legal +permission to copy, distribute and/or modify it. + +For the developers' and authors' protection, the GPL clearly explains that there +is no warranty for this free software. For both users' and authors' sake, the +GPL requires that modified versions be marked as changed, so that their problems +will not be attributed erroneously to authors of previous versions. + +Some devices are designed to deny users access to install or run modified +versions of the software inside them, although the manufacturer can do so. This +is fundamentally incompatible with the aim of protecting users' freedom to +change the software. The systematic pattern of such abuse occurs in the area of +products for individuals to use, which is precisely where it is most +unacceptable. Therefore, we have designed this version of the GPL to prohibit +the practice for those products. If such problems arise substantially in other +domains, we stand ready to extend this provision to those domains in future +versions of the GPL, as needed to protect the freedom of users. + +Finally, every program is threatened constantly by software patents. States +should not allow patents to restrict development and use of software on +general-purpose computers, but in those that do, we wish to avoid the special +danger that patents applied to a free program could make it effectively +proprietary. To prevent this, the GPL assures that patents cannot be used to +render the program non-free. + +The precise terms and conditions for copying, distribution and modification +follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of works, +such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this License. Each +licensee is addressed as "you". "Licensees" and "recipients" may be individuals +or organizations. + +To "modify" a work means to copy from or adapt all or part of the work in a +fashion requiring copyright permission, other than the making of an exact copy. +The resulting work is called a "modified version" of the earlier work or a work +"based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based on the +Program. + +To "propagate" a work means to do anything with it that, without permission, +would make you directly or secondarily liable for infringement under applicable +copyright law, except executing it on a computer or modifying a private copy. +Propagation includes copying, distribution (with or without modification), +making available to the public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other parties to +make or receive copies. Mere interaction with a user through a computer network, +with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" to the extent +that it includes a convenient and prominently visible feature that (1) displays +an appropriate copyright notice, and (2) tells the user that there is no +warranty for the work (except to the extent that warranties are provided), that +licensees may convey the work under this License, and how to view a copy of this +License. If the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work for making +modifications to it. "Object code" means any non-source form of a work. + +A "Standard Interface" means an interface that either is an official standard +defined by a recognized standards body, or, in the case of interfaces specified +for a particular programming language, one that is widely used among developers +working in that language. + +The "System Libraries" of an executable work include anything, other than the +work as a whole, that (a) is included in the normal form of packaging a Major +Component, but which is not part of that Major Component, and (b) serves only to +enable use of the work with that Major Component, or to implement a Standard +Interface for which an implementation is available to the public in source code +form. A "Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system (if any) on +which the executable work runs, or a compiler used to produce the work, or an +object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all the source +code needed to generate, install, and (for an executable work) run the object +code and to modify the work, including scripts to control those activities. +However, it does not include the work's System Libraries, or general-purpose +tools or generally available free programs which are used unmodified in +performing those activities but which are not part of the work. For example, +Corresponding Source includes interface definition files associated with source +files for the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, such as by +intimate data communication or control flow between those subprograms and other +parts of the work. + +The Corresponding Source need not include anything that users can regenerate +automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of copyright on +the Program, and are irrevocable provided the stated conditions are met. This +License explicitly affirms your unlimited permission to run the unmodified +Program. The output from running a covered work is covered by this License only +if the output, given its content, constitutes a covered work. This License +acknowledges your rights of fair use or other equivalent, as provided by +copyright law. + +You may make, run and propagate covered works that you do not convey, without +conditions so long as your license otherwise remains in force. You may convey +covered works to others for the sole purpose of having them make modifications +exclusively for you, or provide you with facilities for running those works, +provided that you comply with the terms of this License in conveying all +material for which you do not control copyright. Those thus making or running +the covered works for you must do so exclusively on your behalf, under your +direction and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the conditions +stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological measure under +any applicable law fulfilling obligations under article 11 of the WIPO copyright +treaty adopted on 20 December 1996, or similar laws prohibiting or restricting +circumvention of such measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention is +effected by exercising rights under this License with respect to the covered +work, and you disclaim any intention to limit operation or modification of the +work as a means of enforcing, against the work's users, your or third parties' +legal rights to forbid circumvention of technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you receive it, +in any medium, provided that you conspicuously and appropriately publish on each +copy an appropriate copyright notice; keep intact all notices stating that this +License and any non-permissive terms added in accord with section 7 apply to the +code; keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, and you may +offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to produce it +from the Program, in the form of source code under the terms of section 4, +provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent works, which +are not by their nature extensions of the covered work, and which are not +combined with it such as to form a larger program, in or on a volume of a +storage or distribution medium, is called an "aggregate" if the compilation and +its resulting copyright are not used to limit the access or legal rights of the +compilation's users beyond what the individual works permit. Inclusion of a +covered work in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of sections 4 +and 5, provided that you also convey the machine-readable Corresponding Source +under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded from the +Corresponding Source as a System Library, need not be included in conveying the +object code work. + +A "User Product" is either (1) a "consumer product", which means any tangible +personal property which is normally used for personal, family, or household +purposes, or (2) anything designed or sold for incorporation into a dwelling. In +determining whether a product is a consumer product, doubtful cases shall be +resolved in favor of coverage. For a particular product received by a particular +user, "normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way in which +the particular user actually uses, or expects or is expected to use, the +product. A product is a consumer product regardless of whether the product has +substantial commercial, industrial or non-consumer uses, unless such uses +represent the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, procedures, +authorization keys, or other information required to install and execute +modified versions of a covered work in that User Product from a modified version +of its Corresponding Source. The information must suffice to ensure that the +continued functioning of the modified object code is in no case prevented or +interfered with solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as part of a +transaction in which the right of possession and use of the User Product is +transferred to the recipient in perpetuity or for a fixed term (regardless of +how the transaction is characterized), the Corresponding Source conveyed under +this section must be accompanied by the Installation Information. But this +requirement does not apply if neither you nor any third party retains the +ability to install modified object code on the User Product (for example, the +work has been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates for a +work that has been modified or installed by the recipient, or for the User +Product in which it has been modified or installed. Access to a network may be +denied when the modification itself materially and adversely affects the +operation of the network or violates the rules and protocols for communication +across the network. + +Corresponding Source conveyed, and Installation Information provided, in accord +with this section must be in a format that is publicly documented (and with an +implementation available to the public in source code form), and must require no +special password or key for unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this License by +making exceptions from one or more of its conditions. Additional permissions +that are applicable to the entire Program shall be treated as though they were +included in this License, to the extent that they are valid under applicable +law. If additional permissions apply only to part of the Program, that part may +be used separately under those permissions, but the entire Program remains +governed by this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option remove any +additional permissions from that copy, or from any part of it. (Additional +permissions may be written to require their own removal in certain cases when +you modify the work.) You may place additional permissions on material, added by +you to a covered work, for which you have or can give appropriate copyright +permission. + +Notwithstanding any other provision of this License, for material you add to a +covered work, you may (if authorized by the copyright holders of that material) +supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further restrictions" +within the meaning of section 10. If the Program as you received it, or any part +of it, contains a notice stating that it is governed by this License along with +a term that is a further restriction, you may remove that term. If a license +document contains a further restriction but permits relicensing or conveying +under this License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does not survive +such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you must place, +in the relevant source files, a statement of the additional terms that apply to +those files, or a notice indicating where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the form of a +separately written license, or stated as exceptions; the above requirements +apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly provided +under this License. Any attempt otherwise to propagate or modify it is void, and +will automatically terminate your rights under this License (including any +patent licenses granted under the third paragraph of section 11). + +However, if you cease all violation of this License, then your license from a +particular copyright holder is reinstated (a) provisionally, unless and until +the copyright holder explicitly and finally terminates your license, and (b) +permanently, if the copyright holder fails to notify you of the violation by +some reasonable means prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is reinstated +permanently if the copyright holder notifies you of the violation by some +reasonable means, this is the first time you have received notice of violation +of this License (for any work) from that copyright holder, and you cure the +violation prior to 30 days after your receipt of the notice. + +Termination of your rights under this section does not terminate the licenses of +parties who have received copies or rights from you under this License. If your +rights have been terminated and not permanently reinstated, you do not qualify +to receive new licenses for the same material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run a copy of +the Program. Ancillary propagation of a covered work occurring solely as a +consequence of using peer-to-peer transmission to receive a copy likewise does +not require acceptance. However, nothing other than this License grants you +permission to propagate or modify any covered work. These actions infringe +copyright if you do not accept this License. Therefore, by modifying or +propagating a covered work, you indicate your acceptance of this License to do +so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically receives a +license from the original licensors, to run, modify and propagate that work, +subject to this License. You are not responsible for enforcing compliance by +third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered work results +from an entity transaction, each party to that transaction who receives a copy +of the work also receives whatever licenses to the work the party's predecessor +in interest had or could give under the previous paragraph, plus a right to +possession of the Corresponding Source of the work from the predecessor in +interest, if the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the rights +granted or affirmed under this License. For example, you may not impose a +license fee, royalty, or other charge for exercise of rights granted under this +License, and you may not initiate litigation (including a cross-claim or +counterclaim in a lawsuit) alleging that any patent claim is infringed by +making, using, selling, offering for sale, or importing the Program or any +portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this License of +the Program or a work on which the Program is based. The work thus licensed is +called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned or +controlled by the contributor, whether already acquired or hereafter acquired, +that would be infringed by some manner, permitted by this License, of making, +using, or selling its contributor version, but do not include claims that would +be infringed only as a consequence of further modification of the contributor +version. For purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free patent +license under the contributor's essential patent claims, to make, use, sell, +offer for sale, import and otherwise run, modify and propagate the contents of +its contributor version. + +In the following three paragraphs, a "patent license" is any express agreement +or commitment, however denominated, not to enforce a patent (such as an express +permission to practice a patent or covenant not to sue for patent infringement). +To "grant" such a patent license to a party means to make such an agreement or +commitment not to enforce a patent against the party. + +If you convey a covered work, knowingly relying on a patent license, and the +Corresponding Source of the work is not available for anyone to copy, free of +charge and under the terms of this License, through a publicly available network +server or other readily accessible means, then you must either (1) cause the +Corresponding Source to be so available, or (2) arrange to deprive yourself of +the benefit of the patent license for this particular work, or (3) arrange, in a +manner consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have actual +knowledge that, but for the patent license, your conveying the covered work in a +country, or your recipient's use of the covered work in a country, would +infringe one or more identifiable patents in that country that you have reason +to believe are valid. + +If, pursuant to or in connection with a single transaction or arrangement, you +convey, or propagate by procuring conveyance of, a covered work, and grant a +patent license to some of the parties receiving the covered work authorizing +them to use, propagate, modify or convey a specific copy of the covered work, +then the patent license you grant is automatically extended to all recipients of +the covered work and works based on it. + +A patent license is "discriminatory" if it does not include within the scope of +its coverage, prohibits the exercise of, or is conditioned on the non-exercise +of one or more of the rights that are specifically granted under this License. +You may not convey a covered work if you are a party to an arrangement with a +third party that is in the business of distributing software, under which you +make payment to the third party based on the extent of your activity of +conveying the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by you (or +copies made from those copies), or (b) primarily for and in connection with +specific products or compilations that contain the covered work, unless you +entered into that arrangement, or that patent license was granted, prior to 28 +March 2007. + +Nothing in this License shall be construed as excluding or limiting any implied +license or other defenses to infringement that may otherwise be available to you +under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not excuse +you from the conditions of this License. If you cannot convey a covered work so +as to satisfy simultaneously your obligations under this License and any other +pertinent obligations, then as a consequence you may not convey it at all. For +example, if you agree to terms that obligate you to collect a royalty for +further conveying from those to whom you convey the Program, the only way you +could satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +13. Use with the GNU Affero General Public License. + +Notwithstanding any other provision of this License, you have permission to link +or combine any covered work with a work licensed under version 3 of the GNU +Affero General Public License into a single combined work, and to convey the +resulting work. The terms of this License will continue to apply to the part +which is the covered work, but the special requirements of the GNU Affero +General Public License, section 13, concerning interaction through a network +will apply to the combination as such. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of the GNU +General Public License from time to time. Such new versions will be similar in +spirit to the present version, but may differ in detail to address new problems +or concerns. + +Each version is given a distinguishing version number. If the Program specifies +that a certain numbered version of the GNU General Public License "or any later +version" applies to it, you have the option of following the terms and +conditions either of that numbered version or of any later version published by +the Free Software Foundation. If the Program does not specify a version number +of the GNU General Public License, you may choose any version ever published by +the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions of the +GNU General Public License can be used, that proxy's public statement of +acceptance of a version permanently authorizes you to choose that version for +the Program. + +Later license versions may give you additional or different permissions. +However, no additional obligations are imposed on any author or copyright holder +as a result of your choosing to follow a later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER +PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER +EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE +QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY +COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS +PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, +INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE +THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED +INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY +HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided above cannot +be given local legal effect according to their terms, reviewing courts shall +apply local law that most closely approximates an absolute waiver of all civil +liability in connection with the Program, unless a warranty or assumption of +liability accompanies a copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use +to the public, the best way to achieve this is to make it free software which +everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach +them to the start of each source file to most effectively state the exclusion of +warranty; and each file should have at least the "copyright" line and a pointer +to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If the program does terminal interaction, make it output a short notice like +this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands might be +different; for a GUI interface, you would use an "about box". + +You should also get your employer (if you work as a programmer) or school, if +any, to sign a "copyright disclaimer" for the program, if necessary. For more +information on this, and how to apply and follow the GNU GPL, see +. + +The GNU General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may consider +it more useful to permit linking proprietary applications with the library. If +this is what you want to do, use the GNU Lesser General Public License instead +of this License. But first, please read +. diff --git a/README.md b/README.md index ed084e90..bef15b51 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Browserosaurus -Browserosaurus is an open-source (MIT license), browser prompter for macOS. It +Browserosaurus is an open-source (GPLv3 license), browser prompter for macOS. It works by setting itself as the default browser; any clicked links in non-browser apps are now sent to Browserosaurus where you are presented with a menu of all your installed browsers. You may now decide which app you’d like to continue diff --git a/docs/supporting-a-browser-or-app.md b/docs/supporting-a-browser-or-app.md index 30c5c18f..e8f06ee3 100644 --- a/docs/supporting-a-browser-or-app.md +++ b/docs/supporting-a-browser-or-app.md @@ -72,7 +72,7 @@ export const apps = { ### Adding a logo All apps must have a logo, that you will no doubt have seen displayed in the -tiles window, when Browserosaurus shows. Most browser logos can be installed +picker window, when Browserosaurus shows. Most browser logos can be installed from an excellent project that contains [almost all browser logos](https://github.com/alrra/browser-logos) by [Cătălin Mariș](https://github.com/alrra). @@ -114,8 +114,8 @@ it behaves how you would expect. Some browsers support opening in a _private_ or _incognito_ mode. Browserosaurus can be set to open the given URL in private mode when holding the -shift key and clicking the tile or using the hotkey. If you'd like to -support this with your added browser, you will need to find the +shift key and clicking an app icon or using its hotkey. If you'd like +to support this with your added browser, you will need to find the [command-line argument](https://en.wikipedia.org/wiki/Command-line_interface#Arguments) that your browser uses when opening URLs from the command-line. In the case of Firefox this is `--private-window`: diff --git a/package-lock.json b/package-lock.json index 9f01140f..8eb7b957 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "browserosaurus", "version": "15.3.10", - "license": "MIT", + "license": "GPL-3.0-only", "dependencies": { "@browser-logos/brave": "^3.0.13", "@browser-logos/brave-beta": "^1.0.11", @@ -39,6 +39,7 @@ "@browser-logos/vivaldi": "^2.1.10", "@browser-logos/vivaldi-snapshot": "^1.0.6", "@browser-logos/yandex": "^1.0.8", + "@heroicons/react": "^1.0.5", "@reduxjs/toolkit": "^1.6.2", "app-exists": "^2.1.1", "axios": "^0.24.0", @@ -52,6 +53,7 @@ "lowdb": "^3.0.0", "p-filter": "^3.0.0", "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.2", "react-redux": "^7.2.6", "redux": "^4.1.2", @@ -64,35 +66,36 @@ "@fullhuman/postcss-purgecss": "^4.0.3", "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^12.1.2", - "@types/jest": "^27.0.2", + "@types/jest": "^27.0.3", "@types/lodash": "^4.14.177", - "@types/node": "^16.11.7", - "@types/react": "^17.0.35", + "@types/node": "^16.11.9", + "@types/react": "^17.0.36", + "@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^17.0.11", "@types/react-redux": "^7.1.20", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", - "@will-stone/eslint-config": "^5.1.0", + "@will-stone/eslint-config": "^6.2.0", "@will-stone/prettier-config": "^6.0.0", "concurrently": "^6.4.0", "copy-webpack-plugin": "^10.0.0", "css-loader": "^6.5.1", - "electron": "^16.0.0", - "eslint": "^7.32.0", + "electron": "^16.0.1", + "eslint": "^8.3.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-jest": "^25.2.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react": "^7.27.1", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-switch-case": "^1.1.2", - "eslint-plugin-unicorn": "^38.0.1", + "eslint-plugin-unicorn": "^39.0.0", "fork-ts-checker-webpack-plugin": "^6.4.0", "husky": "^7.0.4", "jest": "^27.3.1", - "lint-staged": "^12.0.2", + "lint-staged": "^12.1.2", "mini-css-extract-plugin": "^2.4.5", "postcss": "^8.3.11", "postcss-cli": "^9.0.2", @@ -1366,23 +1369,41 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", + "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", "dev": true, "dependencies": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", + "debug": "^4.3.2", + "espree": "^9.0.0", "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/@fullhuman/postcss-purgecss": { @@ -1403,10 +1424,18 @@ "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", "dev": true }, + "node_modules/@heroicons/react": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.5.tgz", + "integrity": "sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==", + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", + "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.0", @@ -1418,9 +1447,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, "node_modules/@istanbuljs/load-nyc-config": { @@ -2326,9 +2355,9 @@ } }, "node_modules/@types/jest": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", - "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", + "version": "27.0.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.3.tgz", + "integrity": "sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==", "dev": true, "dependencies": { "jest-diff": "^27.0.0", @@ -2369,9 +2398,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", + "version": "16.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", + "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", "dev": true }, "node_modules/@types/normalize-package-data": { @@ -2398,15 +2427,24 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "node_modules/@types/react": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz", - "integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==", + "version": "17.0.36", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.36.tgz", + "integrity": "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-beautiful-dnd": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz", + "integrity": "sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", @@ -2993,9 +3031,9 @@ } }, "node_modules/@will-stone/eslint-config": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@will-stone/eslint-config/-/eslint-config-5.1.0.tgz", - "integrity": "sha512-JxOgzI3CuwC7oKurFjKy6nxxdGYly8fCCUxpvhCSukqxKZMgGp7DUber6U3fn1DPZJCpqy48axSUl8XMPADXMw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@will-stone/eslint-config/-/eslint-config-6.2.0.tgz", + "integrity": "sha512-35xaWYarnhIQgfsfbtPVLbPA3t9F/7flzFX2ZdxfRZNNKz7OjR6u1XyoQg5DpgdU4Dao8w5LMCSFfOMdhG1Nxg==", "dev": true, "dependencies": { "confusing-browser-globals": "^1.0.10", @@ -3005,19 +3043,19 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^5.3.0", - "@typescript-eslint/parser": "^5.3.0", - "eslint": "^7.32.0", - "eslint-plugin-import": "^2.25.2", - "eslint-plugin-jest": "^25.2.2", - "eslint-plugin-jsx-a11y": "^6.4.1", + "@typescript-eslint/eslint-plugin": "^5.4.0", + "@typescript-eslint/parser": "^5.4.0", + "eslint": "^8.3.0", + "eslint-plugin-import": "^2.25.3", + "eslint-plugin-jest": "^25.2.4", + "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.26.1", - "eslint-plugin-react-hooks": "^4.2.0", + "eslint-plugin-react": "^7.27.1", + "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-switch-case": "^1.1.2", - "eslint-plugin-unicorn": "^38.0.0", + "eslint-plugin-unicorn": "^39.0.0", "typescript": "4.x" }, "peerDependenciesMeta": { @@ -5131,6 +5169,14 @@ "source-map-resolve": "^0.6.0" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -5771,9 +5817,9 @@ "dev": true }, "node_modules/electron": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.0.tgz", - "integrity": "sha512-B+K/UnEV8NsP7IUOd4VAIYLT0uShLQ/V0p1QQLX0McF8d185AV522faklgMGMtPVWNVL2qifx9rZAsKtHPzmEg==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.1.tgz", + "integrity": "sha512-6TSDBcoKGgmKL/+W+LyaXidRVeRl1V4I81ZOWcqsVksdTMfM4AlxTgfaoYdK/nUhqBrUtuPDcqOyJE6Bc4qMpw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -6494,37 +6540,36 @@ } }, "node_modules/eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", + "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", "dev": true, "dependencies": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", + "@eslint/eslintrc": "^1.0.4", + "@humanwhocodes/config-array": "^0.6.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.1.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", + "glob-parent": "^6.0.1", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -6532,11 +6577,10 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^3.1.0", + "regexpp": "^3.2.0", "semver": "^7.2.1", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, @@ -6544,7 +6588,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6896,9 +6940,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.0.tgz", - "integrity": "sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz", + "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==", "dev": true, "dependencies": { "array-includes": "^3.1.4", @@ -7001,9 +7045,9 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "38.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-38.0.1.tgz", - "integrity": "sha512-eu4HCg7Bv43nk/hYZoWpLzRo668Nb7swQySn94aohn0heh9KLJ1GOFgVxJndLS8BploMGaClxgsyTNGJrP69yw==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-39.0.0.tgz", + "integrity": "sha512-fd5RK2FtYjGcIx3wra7csIE/wkkmBo22T1gZtRTsLr1Mb+KsFKJ+JOdSqhHXQUrI/JTs/Mon64cEYzTgSCbltw==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.14.9", @@ -7139,14 +7183,11 @@ "node": ">=10" } }, - "node_modules/eslint/node_modules/@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.10.4" - } + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "node_modules/eslint/node_modules/chalk": { "version": "4.1.2", @@ -7164,39 +7205,109 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 6" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint/node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, "node_modules/espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", + "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", "dev": true, "dependencies": { - "acorn": "^7.4.0", + "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "eslint-visitor-keys": "^3.1.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/espree/node_modules/acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" } }, "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", "dev": true, "engines": { - "node": ">=4" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/esprima": { @@ -8517,9 +8628,9 @@ } }, "node_modules/globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -11058,9 +11169,9 @@ } }, "node_modules/lilconfig": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", - "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", + "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", "dev": true, "engines": { "node": ">=10" @@ -11073,23 +11184,25 @@ "dev": true }, "node_modules/lint-staged": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.0.2.tgz", - "integrity": "sha512-tpCvACqc7bykziGJmXG0G8YG2RaCrWiDBwmrP9wU7i/3za9JMOvCECQmXjw/sO4ICC70ApVwyqixS1htQX9Haw==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", + "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", "dev": true, "dependencies": { - "cli-truncate": "3.1.0", + "cli-truncate": "^3.1.0", "colorette": "^2.0.16", "commander": "^8.3.0", - "cosmiconfig": "^7.0.1", "debug": "^4.3.2", + "enquirer": "^2.3.6", "execa": "^5.1.1", + "lilconfig": "2.0.4", "listr2": "^3.13.3", "micromatch": "^4.0.4", "normalize-path": "^3.0.0", - "object-inspect": "1.11.0", - "string-argv": "0.3.1", - "supports-color": "9.0.2" + "object-inspect": "^1.11.0", + "string-argv": "^0.3.1", + "supports-color": "^9.0.2", + "yaml": "^1.10.2" }, "bin": { "lint-staged": "bin/lint-staged.js" @@ -11156,22 +11269,6 @@ "node": ">= 12" } }, - "node_modules/lint-staged/node_modules/cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "dependencies": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/lint-staged/node_modules/has-flag": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", @@ -11405,12 +11502,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", @@ -11465,12 +11556,6 @@ "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", "dev": true }, - "node_modules/lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, "node_modules/lodash.zipobject": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz", @@ -11801,6 +11886,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -14180,6 +14270,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -14237,6 +14332,24 @@ "node": ">=0.10.0" } }, + "node_modules/react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "dependencies": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8.5 || ^17.0.0", + "react-dom": "^16.8.5 || ^17.0.0" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -15721,12 +15834,12 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" @@ -15869,62 +15982,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", - "dev": true, - "dependencies": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/table/node_modules/ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/table/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "node_modules/table/node_modules/slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" - } - }, "node_modules/tailwindcss": { "version": "2.2.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.19.tgz", @@ -16254,6 +16311,11 @@ "resolved": "https://registry.npmjs.org/tings/-/tings-3.0.1.tgz", "integrity": "sha512-worujGe5ZC1RFdRJMzGMRCA780PTI+AZhp8DH5iMvnwIFK3k6gE9WQvQdf24P1ZXEBulBVFJey2FhZdxNR/P2w==" }, + "node_modules/tiny-invariant": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -16813,6 +16875,14 @@ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", "dev": true }, + "node_modules/use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0" + } + }, "node_modules/username": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", @@ -18845,20 +18915,37 @@ } }, "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.4.tgz", + "integrity": "sha512-h8Vx6MdxwWI2WM8/zREHMoqdgLNXEL4QX3MWSVMdyNJGvXVOs+6lp+m2hc3FnuMHDc4poxFNI20vCk0OmI4G0Q==", "dev": true, "requires": { "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", + "debug": "^4.3.2", + "espree": "^9.0.0", "globals": "^13.9.0", "ignore": "^4.0.6", "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "minimatch": "^3.0.4", "strip-json-comments": "^3.1.1" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + } } }, "@fullhuman/postcss-purgecss": { @@ -18876,10 +18963,16 @@ "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", "dev": true }, + "@heroicons/react": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-1.0.5.tgz", + "integrity": "sha512-UDMyLM2KavIu2vlWfMspapw9yii7aoLwzI2Hudx4fyoPwfKfxU8r3cL8dEBXOjcLG0/oOONZzbT14M1HoNtEcg==", + "requires": {} + }, "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.6.0.tgz", + "integrity": "sha512-JQlEKbcgEUjBFhLIF4iqM7u/9lwgHRBcpHrmUNCALK0Q3amXN6lxdoXLnF0sm11E9VqTmBALR87IlUg1bZ8A9A==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.0", @@ -18888,9 +18981,9 @@ } }, "@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, "@istanbuljs/load-nyc-config": { @@ -19620,9 +19713,9 @@ } }, "@types/jest": { - "version": "27.0.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.2.tgz", - "integrity": "sha512-4dRxkS/AFX0c5XW6IPMNOydLn2tEhNhJV7DnYK+0bjoJZ+QTmfucBlihX7aoEsh/ocYtkLC73UbnBXBXIxsULA==", + "version": "27.0.3", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-27.0.3.tgz", + "integrity": "sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==", "dev": true, "requires": { "jest-diff": "^27.0.0", @@ -19663,9 +19756,9 @@ "dev": true }, "@types/node": { - "version": "16.11.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", - "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==", + "version": "16.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.9.tgz", + "integrity": "sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A==", "dev": true }, "@types/normalize-package-data": { @@ -19692,15 +19785,24 @@ "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/react": { - "version": "17.0.35", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.35.tgz", - "integrity": "sha512-r3C8/TJuri/SLZiiwwxQoLAoavaczARfT9up9b4Jr65+ErAUX3MIkU0oMOQnrpfgHme8zIqZLX7O5nnjm5Wayw==", + "version": "17.0.36", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.36.tgz", + "integrity": "sha512-CUFUp01OdfbpN/76v4koqgcpcRGT3sYOq3U3N6q0ZVGcyeP40NUdVU+EWe3hs34RNaTefiYyBzOpxBBidCc5zw==", "requires": { "@types/prop-types": "*", "@types/scheduler": "*", "csstype": "^3.0.2" } }, + "@types/react-beautiful-dnd": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.2.tgz", + "integrity": "sha512-+OvPkB8CdE/bGdXKyIhc/Lm2U7UAYCCJgsqmopFmh9gbAudmslkI8eOrPDjg4JhwSE6wytz4a3/wRjKtovHVJg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-dom": { "version": "17.0.11", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.11.tgz", @@ -20136,9 +20238,9 @@ } }, "@will-stone/eslint-config": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@will-stone/eslint-config/-/eslint-config-5.1.0.tgz", - "integrity": "sha512-JxOgzI3CuwC7oKurFjKy6nxxdGYly8fCCUxpvhCSukqxKZMgGp7DUber6U3fn1DPZJCpqy48axSUl8XMPADXMw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@will-stone/eslint-config/-/eslint-config-6.2.0.tgz", + "integrity": "sha512-35xaWYarnhIQgfsfbtPVLbPA3t9F/7flzFX2ZdxfRZNNKz7OjR6u1XyoQg5DpgdU4Dao8w5LMCSFfOMdhG1Nxg==", "dev": true, "requires": { "confusing-browser-globals": "^1.0.10", @@ -21713,6 +21815,14 @@ "source-map-resolve": "^0.6.0" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -22209,9 +22319,9 @@ "dev": true }, "electron": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.0.tgz", - "integrity": "sha512-B+K/UnEV8NsP7IUOd4VAIYLT0uShLQ/V0p1QQLX0McF8d185AV522faklgMGMtPVWNVL2qifx9rZAsKtHPzmEg==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-16.0.1.tgz", + "integrity": "sha512-6TSDBcoKGgmKL/+W+LyaXidRVeRl1V4I81ZOWcqsVksdTMfM4AlxTgfaoYdK/nUhqBrUtuPDcqOyJE6Bc4qMpw==", "dev": true, "requires": { "@electron/get": "^1.13.0", @@ -22769,37 +22879,36 @@ } }, "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.3.0.tgz", + "integrity": "sha512-aIay56Ph6RxOTC7xyr59Kt3ewX185SaGnAr8eWukoPLeriCrvGjvAubxuvaXOfsxhtwV5g0uBOsyhAom4qJdww==", "dev": true, "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", + "@eslint/eslintrc": "^1.0.4", + "@humanwhocodes/config-array": "^0.6.0", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", - "debug": "^4.0.1", + "debug": "^4.3.2", "doctrine": "^3.0.0", "enquirer": "^2.3.5", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", + "eslint-scope": "^7.1.0", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.1.0", + "espree": "^9.1.0", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", + "glob-parent": "^6.0.1", "globals": "^13.6.0", "ignore": "^4.0.6", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", + "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -22807,23 +22916,19 @@ "natural-compare": "^1.4.0", "optionator": "^0.9.1", "progress": "^2.0.0", - "regexpp": "^3.1.0", + "regexpp": "^3.2.0", "semver": "^7.2.1", - "strip-ansi": "^6.0.0", + "strip-ansi": "^6.0.1", "strip-json-comments": "^3.1.0", - "table": "^6.0.9", "text-table": "^0.2.0", "v8-compile-cache": "^2.0.3" }, "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true }, "chalk": { "version": "4.1.2", @@ -22835,13 +22940,52 @@ "supports-color": "^7.1.0" } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "eslint-scope": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz", + "integrity": "sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg==", "dev": true, "requires": { - "is-glob": "^4.0.1" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" } } } @@ -23096,9 +23240,9 @@ } }, "eslint-plugin-react": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.0.tgz", - "integrity": "sha512-0Ut+CkzpppgFtoIhdzi2LpdpxxBvgFf99eFqWxJnUrO7mMe0eOiNpou6rvNYeVVV6lWZvTah0BFne7k5xHjARg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.27.1.tgz", + "integrity": "sha512-meyunDjMMYeWr/4EBLTV1op3iSG3mjT/pz5gti38UzfM4OPpNc2m0t2xvKCOMU5D6FSdd34BIMFOvQbW+i8GAA==", "dev": true, "requires": { "array-includes": "^3.1.4", @@ -23175,9 +23319,9 @@ } }, "eslint-plugin-unicorn": { - "version": "38.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-38.0.1.tgz", - "integrity": "sha512-eu4HCg7Bv43nk/hYZoWpLzRo668Nb7swQySn94aohn0heh9KLJ1GOFgVxJndLS8BploMGaClxgsyTNGJrP69yw==", + "version": "39.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-39.0.0.tgz", + "integrity": "sha512-fd5RK2FtYjGcIx3wra7csIE/wkkmBo22T1gZtRTsLr1Mb+KsFKJ+JOdSqhHXQUrI/JTs/Mon64cEYzTgSCbltw==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.14.9", @@ -23274,20 +23418,26 @@ "dev": true }, "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.1.0.tgz", + "integrity": "sha512-ZgYLvCS1wxOczBYGcQT9DDWgicXwJ4dbocr9uYN+/eresBAUuBu+O4WzB21ufQ/JqQT8gyp7hJ3z8SHii32mTQ==", "dev": true, "requires": { - "acorn": "^7.4.0", + "acorn": "^8.6.0", "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" + "eslint-visitor-keys": "^3.1.0" }, "dependencies": { + "acorn": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.6.0.tgz", + "integrity": "sha512-U1riIR+lBSNi3IbxtaHOIKdH8sLFv3NYfNv8sg7ZsNhcfl4HF2++BfqqrNAxoCLQW1iiylOj76ecnaUxz+z9yw==", + "dev": true + }, "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.1.0.tgz", + "integrity": "sha512-yWJFpu4DtjsWKkt5GeNBBuZMlNcYVs6vRCLoCVEJrTjaSB6LC98gFipNK/erM2Heg/E8mIK+hXG/pJMLK+eRZA==", "dev": true } } @@ -24325,9 +24475,9 @@ } }, "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", + "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -26240,9 +26390,9 @@ } }, "lilconfig": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.3.tgz", - "integrity": "sha512-EHKqr/+ZvdKCifpNrJCKxBTgk5XupZA3y/aCPY9mxfgBzmgh93Mt/WqjjQ38oMxXuvDokaKiM3lAgvSH2sjtHg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz", + "integrity": "sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==", "dev": true }, "lines-and-columns": { @@ -26252,23 +26402,25 @@ "dev": true }, "lint-staged": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.0.2.tgz", - "integrity": "sha512-tpCvACqc7bykziGJmXG0G8YG2RaCrWiDBwmrP9wU7i/3za9JMOvCECQmXjw/sO4ICC70ApVwyqixS1htQX9Haw==", + "version": "12.1.2", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.1.2.tgz", + "integrity": "sha512-bSMcQVqMW98HLLLR2c2tZ+vnDCnx4fd+0QJBQgN/4XkdspGRPc8DGp7UuOEBe1ApCfJ+wXXumYnJmU+wDo7j9A==", "dev": true, "requires": { - "cli-truncate": "3.1.0", + "cli-truncate": "^3.1.0", "colorette": "^2.0.16", "commander": "^8.3.0", - "cosmiconfig": "^7.0.1", "debug": "^4.3.2", + "enquirer": "^2.3.6", "execa": "^5.1.1", + "lilconfig": "2.0.4", "listr2": "^3.13.3", "micromatch": "^4.0.4", "normalize-path": "^3.0.0", - "object-inspect": "1.11.0", - "string-argv": "0.3.1", - "supports-color": "9.0.2" + "object-inspect": "^1.11.0", + "string-argv": "^0.3.1", + "supports-color": "^9.0.2", + "yaml": "^1.10.2" }, "dependencies": { "ansi-regex": { @@ -26305,19 +26457,6 @@ "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", "dev": true }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, "has-flag": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", @@ -26488,12 +26627,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", @@ -26548,12 +26681,6 @@ "integrity": "sha1-NhY1Hzu6YZlKCTGYlmC9AyVP0Ak=", "dev": true }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", - "dev": true - }, "lodash.zipobject": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/lodash.zipobject/-/lodash.zipobject-4.1.3.tgz", @@ -26804,6 +26931,11 @@ "fs-monkey": "1.0.3" } }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", @@ -28541,6 +28673,11 @@ "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", "dev": true }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -28586,6 +28723,20 @@ "object-assign": "^4.1.1" } }, + "react-beautiful-dnd": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.0.tgz", + "integrity": "sha512-aGvblPZTJowOWUNiwd6tNfEpgkX5OxmpqxHKNW/4VmvZTNTbeiq7bA3bn5T+QSF2uibXB0D1DmJsb1aC/+3cUA==", + "requires": { + "@babel/runtime": "^7.9.2", + "css-box-model": "^1.2.0", + "memoize-one": "^5.1.1", + "raf-schd": "^4.0.2", + "react-redux": "^7.2.0", + "redux": "^4.0.4", + "use-memo-one": "^1.1.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -29804,12 +29955,12 @@ } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, "strip-bom": { @@ -29909,51 +30060,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", - "dev": true, - "requires": { - "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", - "lodash.truncate": "^4.4.2", - "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "slice-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - } - } - } - }, "tailwindcss": { "version": "2.2.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-2.2.19.tgz", @@ -30223,6 +30329,11 @@ "resolved": "https://registry.npmjs.org/tings/-/tings-3.0.1.tgz", "integrity": "sha512-worujGe5ZC1RFdRJMzGMRCA780PTI+AZhp8DH5iMvnwIFK3k6gE9WQvQdf24P1ZXEBulBVFJey2FhZdxNR/P2w==" }, + "tiny-invariant": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz", + "integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -30622,6 +30733,12 @@ "prepend-http": "^2.0.0" } }, + "use-memo-one": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", + "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==", + "requires": {} + }, "username": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/username/-/username-5.1.0.tgz", diff --git a/package.json b/package.json index a5c365d5..9863532a 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "url": "https://github.com/will-stone/browserosaurus/issues" }, "repository": "https://github.com/will-stone/browserosaurus", - "license": "MIT", + "license": "GPL-3.0-only", "author": "Will Stone", "main": ".webpack/main", "scripts": { @@ -49,12 +49,12 @@ "asar": true, "appCategoryType": "public.app-category.developer-tools", "packageManager": "npm", - "extendInfo": "config/Info.plist", + "extendInfo": "plist/Info.plist", "osxSign": { "gatekeeper-assess": false, "hardened-runtime": true, - "entitlements": "config/entitlements.mac.plist", - "entitlements-inherit": "config/entitlements.mac.plist" + "entitlements": "plist/entitlements.mac.plist", + "entitlements-inherit": "plist/entitlements.mac.plist" }, "icon": "src/shared/static/icon/icon.icns", "protocols": [ @@ -90,9 +90,9 @@ "config": "./webpack.renderer.config.cjs", "entryPoints": [ { - "html": "./src/renderers/tiles/index.html", - "js": "./src/renderers/tiles/index.tsx", - "name": "tiles_window", + "html": "./src/renderers/picker/index.html", + "js": "./src/renderers/picker/index.tsx", + "name": "picker_window", "preload": { "js": "./src/renderers/shared/preload.ts" } @@ -174,6 +174,7 @@ "@browser-logos/vivaldi": "^2.1.10", "@browser-logos/vivaldi-snapshot": "^1.0.6", "@browser-logos/yandex": "^1.0.8", + "@heroicons/react": "^1.0.5", "@reduxjs/toolkit": "^1.6.2", "app-exists": "^2.1.1", "axios": "^0.24.0", @@ -187,6 +188,7 @@ "lowdb": "^3.0.0", "p-filter": "^3.0.0", "react": "^17.0.2", + "react-beautiful-dnd": "^13.1.0", "react-dom": "^17.0.2", "react-redux": "^7.2.6", "redux": "^4.1.2", @@ -199,35 +201,36 @@ "@fullhuman/postcss-purgecss": "^4.0.3", "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^12.1.2", - "@types/jest": "^27.0.2", + "@types/jest": "^27.0.3", "@types/lodash": "^4.14.177", - "@types/node": "^16.11.7", - "@types/react": "^17.0.35", + "@types/node": "^16.11.9", + "@types/react": "^17.0.36", + "@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^17.0.11", "@types/react-redux": "^7.1.20", "@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/parser": "^5.4.0", - "@will-stone/eslint-config": "^5.1.0", + "@will-stone/eslint-config": "^6.2.0", "@will-stone/prettier-config": "^6.0.0", "concurrently": "^6.4.0", "copy-webpack-plugin": "^10.0.0", "css-loader": "^6.5.1", - "electron": "^16.0.0", - "eslint": "^7.32.0", + "electron": "^16.0.1", + "eslint": "^8.3.0", "eslint-plugin-import": "^2.25.3", "eslint-plugin-jest": "^25.2.4", "eslint-plugin-jsx-a11y": "^6.5.1", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react": "^7.27.1", "eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-simple-import-sort": "^7.0.0", "eslint-plugin-switch-case": "^1.1.2", - "eslint-plugin-unicorn": "^38.0.1", + "eslint-plugin-unicorn": "^39.0.0", "fork-ts-checker-webpack-plugin": "^6.4.0", "husky": "^7.0.4", "jest": "^27.3.1", - "lint-staged": "^12.0.2", + "lint-staged": "^12.1.2", "mini-css-extract-plugin": "^2.4.5", "postcss": "^8.3.11", "postcss-cli": "^9.0.2", diff --git a/config/Info.plist b/plist/Info.plist similarity index 100% rename from config/Info.plist rename to plist/Info.plist diff --git a/config/entitlements.mac.plist b/plist/entitlements.mac.plist similarity index 100% rename from config/entitlements.mac.plist rename to plist/entitlements.mac.plist diff --git a/src/main/main.ts b/src/main/main.ts index 70f64e14..462e832b 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -33,4 +33,4 @@ app.on('open-url', (event, url) => { * Enter actions from renderer into main's store's queue */ electron.ipcMain.on(Channel.PREFS, (_, action: AnyAction) => dispatch(action)) -electron.ipcMain.on(Channel.TILES, (_, action: AnyAction) => dispatch(action)) +electron.ipcMain.on(Channel.PICKER, (_, action: AnyAction) => dispatch(action)) diff --git a/src/main/state/middleware.action-hub.ts b/src/main/state/middleware.action-hub.ts index b5379fe3..ecb92bfd 100644 --- a/src/main/state/middleware.action-hub.ts +++ b/src/main/state/middleware.action-hub.ts @@ -2,7 +2,7 @@ /* eslint-disable unicorn/prefer-regexp-test -- rtk uses .match */ import type { AnyAction, Middleware } from '@reduxjs/toolkit' import { execFile } from 'child_process' -import { app, autoUpdater, shell } from 'electron' +import { app, autoUpdater, Notification, shell } from 'electron' import deepEqual from 'fast-deep-equal' import path from 'path' @@ -11,25 +11,24 @@ import { apps } from '../../config/apps' import { B_URL, ISSUES_URL } from '../../config/CONSTANTS' import { appReady, - clickedCopyButton, + clickedApp, clickedHomepageButton, clickedOpenIssueButton, clickedRescanApps, + clickedRestorePicker, clickedSetAsDefaultBrowserButton, - clickedTile, - clickedUpdateAvailableButton, clickedUpdateButton, clickedUpdateRestartButton, + clickedUrlBar, gotAppVersion, gotDefaultBrowserStatus, + pickerStarted, prefsStarted, pressedAppKey, pressedCopyKey, pressedEscapeKey, - syncAppIds, syncData, syncStorage, - tilesStarted, updateAvailable, updateDownloaded, updateDownloading, @@ -42,7 +41,12 @@ import { createTray, tray } from '../tray' import copyToClipboard from '../utils/copy-to-clipboard' import { getUpdateUrl } from '../utils/get-update-url' import { isUpdateAvailable } from '../utils/is-update-available' -import { createWindows, pWindow, showTWindow, tWindow } from '../windows' +import { + createWindows, + pickerWindow, + prefsWindow, + showPickerWindow, +} from '../windows' import { checkForUpdate } from './thunk.check-for-update' import { getInstalledAppIds } from './thunk.get-installed-app-ids' @@ -88,8 +92,8 @@ export const actionHubMiddleware = autoUpdater.on('before-quit-for-update', () => { // All windows must be closed before an update can be applied using "restart". - tWindow?.destroy() - pWindow?.destroy() + pickerWindow?.destroy() + prefsWindow?.destroy() }) autoUpdater.on('update-available', () => { @@ -133,17 +137,23 @@ export const actionHubMiddleware = dispatch(checkForUpdate() as unknown as AnyAction) } - // When a renderer starts, send down all the local store for synchonisation - else if (tilesStarted.match(action) || prefsStarted.match(action)) { - dispatch(syncAppIds(nextState.appIds)) + // When a renderer starts, send down all the local store for synchronisation + else if (pickerStarted.match(action) || prefsStarted.match(action)) { dispatch(syncData(nextState.data)) dispatch(syncStorage(nextState.storage)) } // Copy to clipboard - else if (clickedCopyButton.match(action) || pressedCopyKey.match(action)) { - copyToClipboard(action.payload) - tWindow?.hide() + else if (clickedUrlBar.match(action) || pressedCopyKey.match(action)) { + if (nextState.data.url) { + copyToClipboard(nextState.data.url) + pickerWindow?.hide() + new Notification({ + title: 'Browserosaurus', + body: 'URL copied to clipboard', + silent: true, + }).show() + } } // Set as default browser @@ -166,9 +176,9 @@ export const actionHubMiddleware = else if (clickedUpdateRestartButton.match(action)) { autoUpdater.quitAndInstall() // @ts-expect-error -- window must be destroyed to prevent race condition - pWindow = null + prefsWindow = null // @ts-expect-error -- window must be destroyed to prevent race condition - tWindow = null + pickerWindow = null // https://stackoverflow.com/questions/38309240/object-has-been-destroyed-when-open-secondary-child-window-in-electron-js } @@ -179,7 +189,7 @@ export const actionHubMiddleware = } // Open app - else if (pressedAppKey.match(action) || clickedTile.match(action)) { + else if (pressedAppKey.match(action) || clickedApp.match(action)) { const { appId, url = '', isAlt, isShift } = action.payload // Bail if app's bundle id is missing @@ -208,17 +218,22 @@ export const actionHubMiddleware = execFile('open', openArguments) - tWindow?.hide() + pickerWindow?.hide() } // Escape key else if (pressedEscapeKey.match(action)) { - tWindow?.hide() + pickerWindow?.hide() } // Open URL else if (urlOpened.match(action)) { - showTWindow() + showPickerWindow() + } + + // Tray: restore picker + else if (clickedRestorePicker.match(action)) { + showPickerWindow() } // Open homepage @@ -226,15 +241,10 @@ export const actionHubMiddleware = shell.openExternal(B_URL) } - // Open homepage + // Open issues page else if (clickedOpenIssueButton.match(action)) { shell.openExternal(ISSUES_URL) } - // Open homepage - else if (clickedUpdateAvailableButton.match(action)) { - pWindow?.show() - } - return result } diff --git a/src/main/state/middleware.bus.ts b/src/main/state/middleware.bus.ts index e830abcb..015aeeeb 100644 --- a/src/main/state/middleware.bus.ts +++ b/src/main/state/middleware.bus.ts @@ -2,7 +2,7 @@ import type { AnyAction, Middleware } from '@reduxjs/toolkit' import { Channel } from '../../shared/state/channels' import type { RootState } from '../../shared/state/reducer.root' -import { pWindow, tWindow } from '../windows' +import { pickerWindow, prefsWindow } from '../windows' /** * Pass actions between main and renderers @@ -24,16 +24,16 @@ export const busMiddleware = // Send actions from main to all renderers if (action.type.startsWith(Channel.MAIN)) { - tWindow?.webContents.send(Channel.MAIN, action) - pWindow?.webContents.send(Channel.MAIN, action) + pickerWindow?.webContents.send(Channel.MAIN, action) + prefsWindow?.webContents.send(Channel.MAIN, action) } - // Send actions from prefs to tiles + // Send actions from prefs to picker else if (action.type.startsWith(Channel.PREFS)) { - tWindow?.webContents.send(Channel.MAIN, action) + pickerWindow?.webContents.send(Channel.MAIN, action) } - // Send actions from tiles to prefs - else if (action.type.startsWith(Channel.TILES)) { - pWindow?.webContents.send(Channel.MAIN, action) + // Send actions from picker to prefs + else if (action.type.startsWith(Channel.PICKER)) { + prefsWindow?.webContents.send(Channel.MAIN, action) } return result diff --git a/src/main/state/thunk.get-installed-app-ids.ts b/src/main/state/thunk.get-installed-app-ids.ts index 1a3516a1..28a6b414 100644 --- a/src/main/state/thunk.get-installed-app-ids.ts +++ b/src/main/state/thunk.get-installed-app-ids.ts @@ -1,7 +1,7 @@ import sleep from 'tings/sleep' import { apps } from '../../config/apps' -import { syncAppIds } from '../../shared/state/actions' +import { installedAppsRetrieved } from '../../shared/state/actions' import type { AppThunk } from '../../shared/state/reducer.root' import { filterAppsByInstalled } from '../utils/filter-apps-by-installed' @@ -14,6 +14,6 @@ export const getInstalledAppIds = (): AppThunk => async (dispatch) => { await sleep(500) dispatch(getInstalledAppIds()) } else { - dispatch(syncAppIds(installedApps)) + dispatch(installedAppsRetrieved(installedApps)) } } diff --git a/src/main/state/thunk.url-opener.ts b/src/main/state/thunk.url-opener.ts index 79b01bc4..4b3e06b6 100644 --- a/src/main/state/thunk.url-opener.ts +++ b/src/main/state/thunk.url-opener.ts @@ -6,10 +6,10 @@ import type { AppThunk } from '../../shared/state/reducer.root' export const urlOpener = (url: string): AppThunk => async (dispatch, getState) => { - if (getState().data.tilesStarted) { + if (getState().data.pickerStarted) { dispatch(urlOpened(url)) } - // The `open-url` electron.app event can get fired before the tile window is + // The `open-url` electron.app event can get fired before the picker window is // ready, if B was opened by sending it a URL. Here we wait before trying again. else { await sleep(500) diff --git a/src/main/storage.ts b/src/main/storage.ts index ec18655c..0ba9d46b 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -41,19 +41,17 @@ export const storage = { return defaultStorage } - database.data = { - ...defaultStorage, - ...database.data, - } - // Removes unknown keys in storage for (const key of keys(database.data)) { - if (typeof defaultStorage[key] === undefined) { + if (typeof defaultStorage[key] === 'undefined') { delete database.data[key] } } - return database.data + return { + ...defaultStorage, + ...database.data, + } }, setAll: (value: Storage): void => { diff --git a/src/main/tray.ts b/src/main/tray.ts index 6ff23550..9abf2302 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -1,7 +1,9 @@ import { app, Menu, Tray } from 'electron' import path from 'path' -import { pWindow, showTWindow } from './windows' +import { clickedRestorePicker } from '../shared/state/actions' +import { dispatch } from './state/store' +import { prefsWindow } from './windows' export let tray: Tray | undefined @@ -21,14 +23,14 @@ export function createTray(): void { Menu.buildFromTemplate([ { label: 'Restore recently closed URL', - click: () => showTWindow(), + click: () => dispatch(clickedRestorePicker()), }, { type: 'separator', }, { label: 'Preferences...', - click: () => pWindow?.show(), + click: () => prefsWindow?.show(), }, { type: 'separator', diff --git a/src/main/windows.ts b/src/main/windows.ts index b0465ac1..a91df57f 100644 --- a/src/main/windows.ts +++ b/src/main/windows.ts @@ -3,22 +3,22 @@ import path from 'path' import { gotDefaultBrowserStatus, - tWindowBoundsChanged, + pickerWindowBoundsChanged, } from '../shared/state/actions' import { dispatch } from './state/store' import { storage } from './storage' -declare const TILES_WINDOW_WEBPACK_ENTRY: string -declare const TILES_WINDOW_PRELOAD_WEBPACK_ENTRY: string +declare const PICKER_WINDOW_WEBPACK_ENTRY: string +declare const PICKER_WINDOW_PRELOAD_WEBPACK_ENTRY: string declare const PREFS_WINDOW_WEBPACK_ENTRY: string declare const PREFS_WINDOW_PRELOAD_WEBPACK_ENTRY: string // Prevents garbage collection -export let tWindow: BrowserWindow | null | undefined -export let pWindow: BrowserWindow | null | undefined +export let pickerWindow: BrowserWindow | null | undefined +export let prefsWindow: BrowserWindow | null | undefined export async function createWindows(): Promise { - pWindow = new BrowserWindow({ + prefsWindow = new BrowserWindow({ // Only show on demand show: false, @@ -48,30 +48,29 @@ export async function createWindows(): Promise { }, }) - pWindow.on('hide', () => { - pWindow?.hide() + prefsWindow.on('hide', () => { + prefsWindow?.hide() }) - pWindow.on('close', (event_) => { + prefsWindow.on('close', (event_) => { event_.preventDefault() - pWindow?.hide() + prefsWindow?.hide() }) - pWindow.on('show', () => { + prefsWindow.on('show', () => { // There isn't a listener for default protocol client, therefore the check // is made each time the window is brought into focus. dispatch(gotDefaultBrowserStatus(app.isDefaultProtocolClient('http'))) }) - const width = storage.get('width') const height = storage.get('height') - tWindow = new BrowserWindow({ + pickerWindow = new BrowserWindow({ frame: true, icon: path.join(__dirname, '/static/icon/icon.png'), title: 'Browserosaurus', webPreferences: { - preload: TILES_WINDOW_PRELOAD_WEBPACK_ENTRY, + preload: PICKER_WINDOW_PRELOAD_WEBPACK_ENTRY, nodeIntegration: false, nodeIntegrationInWorker: false, nodeIntegrationInSubFrames: false, @@ -79,9 +78,10 @@ export async function createWindows(): Promise { }, center: true, height, - minHeight: 204, - width, - minWidth: 424, + minHeight: 250, + width: 375, + maxWidth: 375, + minWidth: 375, show: false, minimizable: false, maximizable: false, @@ -97,37 +97,37 @@ export async function createWindows(): Promise { alwaysOnTop: true, }) - tWindow.setWindowButtonVisibility(false) + pickerWindow.setWindowButtonVisibility(false) - tWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) + pickerWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }) - tWindow.on('hide', () => { - tWindow?.hide() + pickerWindow.on('hide', () => { + pickerWindow?.hide() }) - tWindow.on('close', (event_) => { + pickerWindow.on('close', (event_) => { event_.preventDefault() - tWindow?.hide() + pickerWindow?.hide() }) - tWindow.on('resize', () => { - if (tWindow) { - dispatch(tWindowBoundsChanged(tWindow.getBounds())) + pickerWindow.on('resize', () => { + if (pickerWindow) { + dispatch(pickerWindowBoundsChanged(pickerWindow.getBounds())) } }) - tWindow.on('blur', () => { - tWindow?.hide() + pickerWindow.on('blur', () => { + pickerWindow?.hide() }) await Promise.all([ - pWindow.loadURL(PREFS_WINDOW_WEBPACK_ENTRY), - tWindow.loadURL(TILES_WINDOW_WEBPACK_ENTRY), + prefsWindow.loadURL(PREFS_WINDOW_WEBPACK_ENTRY), + pickerWindow.loadURL(PICKER_WINDOW_WEBPACK_ENTRY), ]) } -export function showTWindow(): void { - if (tWindow) { +export function showPickerWindow(): void { + if (pickerWindow) { const displayBounds = screen.getDisplayNearestPoint( screen.getCursorScreenPoint(), ).bounds @@ -139,7 +139,7 @@ export function showTWindow(): void { const mousePoint = screen.getCursorScreenPoint() - const bWindowBounds = tWindow.getBounds() + const bWindowBounds = pickerWindow.getBounds() const bWindowEdges = { right: mousePoint.x + bWindowBounds.width, @@ -162,8 +162,8 @@ export function showTWindow(): void { : mousePoint.y + nudge.y, } - tWindow.setPosition(inWindowPosition.x, inWindowPosition.y, false) + pickerWindow.setPosition(inWindowPosition.x, inWindowPosition.y, false) - tWindow.show() + pickerWindow.show() } } diff --git a/src/renderers/tiles/components/_bootstrap.tsx b/src/renderers/picker/components/_bootstrap.tsx similarity index 86% rename from src/renderers/tiles/components/_bootstrap.tsx rename to src/renderers/picker/components/_bootstrap.tsx index 17cb83ea..c93fe836 100644 --- a/src/renderers/tiles/components/_bootstrap.tsx +++ b/src/renderers/picker/components/_bootstrap.tsx @@ -1,7 +1,7 @@ import React from 'react' import { Provider } from 'react-redux' -import store from '../store' +import store from '../state/store' import App from './layout' const Bootstrap: React.FC = () => { diff --git a/src/renderers/tiles/components/atoms/app-logo.tsx b/src/renderers/picker/components/atoms/app-logo.tsx similarity index 59% rename from src/renderers/tiles/components/atoms/app-logo.tsx rename to src/renderers/picker/components/atoms/app-logo.tsx index 72aca91a..35b3ec67 100644 --- a/src/renderers/tiles/components/atoms/app-logo.tsx +++ b/src/renderers/picker/components/atoms/app-logo.tsx @@ -6,15 +6,12 @@ import type { InstalledApp } from '../../../../shared/state/hooks' interface Props extends React.ComponentPropsWithoutRef<'img'> { app: InstalledApp + className?: string } -const AppLogo = ({ app }: Props): JSX.Element => { +const AppLogo = ({ app, className }: Props): JSX.Element => { return ( - {app.name} + {app.name} ) } diff --git a/src/renderers/tiles/components/atoms/kbd.tsx b/src/renderers/picker/components/atoms/kbd.tsx similarity index 69% rename from src/renderers/tiles/components/atoms/kbd.tsx rename to src/renderers/picker/components/atoms/kbd.tsx index ae2d63fa..fbd3d8d0 100644 --- a/src/renderers/tiles/components/atoms/kbd.tsx +++ b/src/renderers/picker/components/atoms/kbd.tsx @@ -12,7 +12,8 @@ const Kbd = ({ children, className, style }: Props): JSX.Element => { diff --git a/src/renderers/tiles/components/hooks/use-keyboard-events.ts b/src/renderers/picker/components/hooks/use-keyboard-events.ts similarity index 71% rename from src/renderers/tiles/components/hooks/use-keyboard-events.ts rename to src/renderers/picker/components/hooks/use-keyboard-events.ts index 3586696f..b42d885c 100644 --- a/src/renderers/tiles/components/hooks/use-keyboard-events.ts +++ b/src/renderers/picker/components/hooks/use-keyboard-events.ts @@ -13,19 +13,12 @@ const keyboardEvent = (event: KeyboardEvent): AppThunk => (dispatch, getState) => { const { url } = getState().data - const { hotkeys, fav } = getState().storage + const { apps } = getState().storage const virtualKey = event.key.toLowerCase() // Not needed at the moment but useful to know // const physicalKey = event.code.toLowerCase() - // Favourite hotkeys - // Enter and space can cause previously focussed items to activate so are - // therefore always disabled. - if (virtualKey === ' ' || virtualKey === 'enter') { - event.preventDefault() - } - // Escape if (virtualKey === 'escape') { dispatch(pressedEscapeKey()) @@ -48,31 +41,18 @@ const keyboardEvent = // App hotkey else if (!event.metaKey && /^([a-z0-9])$/u.test(virtualKey)) { event.preventDefault() - const appId = hotkeys[virtualKey] - if (appId) { + const foundApp = apps.find((app) => app.hotkey === virtualKey) + if (foundApp) { dispatch( pressedAppKey({ url, - appId, + appId: foundApp.id, isAlt: event.altKey, isShift: event.shiftKey, }), ) } } - - // Favourite hotkeys - else if (virtualKey === ' ' || virtualKey === 'enter') { - event.preventDefault() - dispatch( - pressedAppKey({ - url, - appId: fav, - isAlt: event.altKey, - isShift: event.shiftKey, - }), - ) - } } export const useKeyboardEvents = (): void => { diff --git a/src/renderers/picker/components/layout.tsx b/src/renderers/picker/components/layout.tsx new file mode 100644 index 00000000..20b6413e --- /dev/null +++ b/src/renderers/picker/components/layout.tsx @@ -0,0 +1,76 @@ +import React, { useEffect } from 'react' +import { useDispatch } from 'react-redux' + +import { pickerStarted } from '../../../shared/state/actions' +import { useInstalledApps } from '../../../shared/state/hooks' +import { favAppRef } from '../refs' +import AppLogo from './atoms/app-logo' +import Kbd from './atoms/kbd' +import { useKeyboardEvents } from './hooks/use-keyboard-events' +import { AppButton } from './molecules/app-button' +import SupportMessage from './organisms/support-message' +import UrlBar from './organisms/url-bar' + +const useAppStarted = () => { + const dispatch = useDispatch() + useEffect(() => { + dispatch(pickerStarted()) + }, [dispatch]) +} + +const App: React.FC = () => { + /** + * Tell main that renderer is available + */ + useAppStarted() + + /** + * Setup keyboard listeners + */ + useKeyboardEvents() + + const [favApp, ...normalApps] = useInstalledApps() + + return ( +
+
+
+ {favApp && ( + + + {favApp.name} + {favApp.hotkey && {favApp.hotkey}} + + )} +
+ +
+ {normalApps.map((app, index) => { + const key = app.id + index + return ( + + + {app.hotkey && ( + {app.hotkey} + )} + {app.name} + + ) + })} +
+
+ + +
+ ) +} + +export default App diff --git a/src/renderers/picker/components/molecules/app-button.tsx b/src/renderers/picker/components/molecules/app-button.tsx new file mode 100644 index 00000000..6d6043e5 --- /dev/null +++ b/src/renderers/picker/components/molecules/app-button.tsx @@ -0,0 +1,56 @@ +import clsx from 'clsx' +import React from 'react' +import { useDispatch } from 'react-redux' + +import { clickedApp } from '../../../../shared/state/actions' +import type { InstalledApp } from '../../../../shared/state/hooks' +import { useSelector } from '../../../../shared/state/hooks' + +interface Props { + app: InstalledApp + children?: React.ReactNode + className?: string +} + +export const AppButton = React.forwardRef( + ( + { children, app, className }: Props, + ref: React.ForwardedRef, + ): JSX.Element => { + const dispatch = useDispatch() + const url = useSelector((state) => state.data.url) + + return ( + + ) + }, +) + +AppButton.displayName = 'AppButton' diff --git a/src/renderers/picker/components/organisms/apps.test.tsx b/src/renderers/picker/components/organisms/apps.test.tsx new file mode 100644 index 00000000..0f8af66d --- /dev/null +++ b/src/renderers/picker/components/organisms/apps.test.tsx @@ -0,0 +1,212 @@ +import '../../../shared/preload' + +import { fireEvent, render, screen } from '@testing-library/react' +import electron from 'electron' +import React from 'react' + +import { + clickedApp, + installedAppsRetrieved, + pressedAppKey, + reorderedApps, + syncStorage, + urlOpened, +} from '../../../../shared/state/actions' +import { Channel } from '../../../../shared/state/channels' +import Wrapper from '../_bootstrap' + +test('apps', () => { + render() + const win = new electron.BrowserWindow() + win.webContents.send( + Channel.MAIN, + installedAppsRetrieved([ + 'org.mozilla.firefox', + 'com.apple.Safari', + 'com.brave.Browser.nightly', + ]), + ) + // Check apps and app logos shown + expect(screen.getByText('Firefox')).toBeVisible() + expect(screen.getByRole('button', { name: 'Firefox App' })).toBeVisible() + expect(screen.getByAltText('Safari')).toBeVisible() + expect(screen.getByRole('button', { name: 'Safari App' })).toBeVisible() + expect(screen.getByAltText('Brave Nightly')).toBeVisible() + expect( + screen.getByRole('button', { name: 'Brave Nightly App' }), + ).toBeVisible() + + expect(screen.getAllByRole('button', { name: /[A-z]+ App/u })).toHaveLength(3) + + win.webContents.send( + Channel.MAIN, + syncStorage({ + apps: [ + { id: 'org.mozilla.firefox', hotkey: null }, + { id: 'com.apple.Safari', hotkey: null }, + { id: 'com.brave.Browser.nightly', hotkey: null }, + ], + supportMessage: -1, + height: 200, + firstRun: false, + }), + ) + + // Correct info sent to main when app clicked + fireEvent.click(screen.getByRole('button', { name: 'Firefox App' })) + expect(electron.ipcRenderer.send).toHaveBeenCalledWith( + Channel.PICKER, + clickedApp({ + url: '', + appId: 'org.mozilla.firefox', + isAlt: false, + isShift: false, + }), + ) + + // Correct info sent to main when app clicked + const url = 'http://example.com' + win.webContents.send(Channel.MAIN, urlOpened(url)) + fireEvent.click(screen.getByRole('button', { name: 'Brave Nightly App' }), { + altKey: true, + }) + expect(electron.ipcRenderer.send).toHaveBeenCalledWith( + Channel.PICKER, + clickedApp({ + url, + appId: 'com.brave.Browser.nightly', + isAlt: true, + isShift: false, + }), + ) +}) + +test('use hotkey', () => { + render() + const win = new electron.BrowserWindow() + win.webContents.send( + Channel.MAIN, + installedAppsRetrieved(['com.apple.Safari']), + ) + win.webContents.send( + Channel.MAIN, + syncStorage({ + apps: [{ id: 'com.apple.Safari', hotkey: 's' }], + supportMessage: -1, + height: 200, + firstRun: false, + }), + ) + + const url = 'http://example.com' + win.webContents.send(Channel.MAIN, urlOpened(url)) + fireEvent.keyDown(document, { key: 'S', code: 'KeyS', keyCode: 83 }) + expect(electron.ipcRenderer.send).toHaveBeenCalledWith( + Channel.PICKER, + pressedAppKey({ + url, + appId: 'com.apple.Safari', + isAlt: false, + isShift: false, + }), + ) +}) + +test('use hotkey with alt', () => { + render() + const win = new electron.BrowserWindow() + win.webContents.send( + Channel.MAIN, + installedAppsRetrieved(['com.apple.Safari']), + ) + + win.webContents.send( + Channel.MAIN, + syncStorage({ + apps: [{ id: 'com.apple.Safari', hotkey: 's' }], + supportMessage: -1, + height: 200, + firstRun: false, + }), + ) + + const url = 'http://example.com' + win.webContents.send(Channel.MAIN, urlOpened(url)) + fireEvent.keyDown(document, { + key: 's', + code: 'KeyS', + keyCode: 83, + altKey: true, + }) + expect(electron.ipcRenderer.send).toHaveBeenCalledWith( + Channel.PICKER, + pressedAppKey({ + url, + appId: 'com.apple.Safari', + isAlt: true, + isShift: false, + }), + ) +}) + +test('hold shift', () => { + render() + const win = new electron.BrowserWindow() + win.webContents.send( + Channel.MAIN, + installedAppsRetrieved(['org.mozilla.firefox']), + ) + const url = 'http://example.com' + win.webContents.send(Channel.MAIN, urlOpened(url)) + fireEvent.click(screen.getByRole('button', { name: 'Firefox App' }), { + shiftKey: true, + }) + expect(electron.ipcRenderer.send).toHaveBeenCalledWith( + Channel.PICKER, + clickedApp({ + url, + appId: 'org.mozilla.firefox', + isAlt: false, + isShift: true, + }), + ) +}) + +test('tiles order', () => { + render() + const win = new electron.BrowserWindow() + win.webContents.send( + Channel.MAIN, + installedAppsRetrieved([ + 'org.mozilla.firefox', + 'com.apple.Safari', + 'com.operasoftware.Opera', + 'com.microsoft.edgemac', + 'com.brave.Browser', + ]), + ) + // Check tiles and tile logos shown + const apps = screen.getAllByRole('button', { name: /[A-z]+ App/u }) + + expect(apps).toHaveLength(5) + + win.webContents.send( + Channel.MAIN, + reorderedApps({ source: 1, destination: 0 }), + ) + win.webContents.send( + Channel.MAIN, + reorderedApps({ source: 2, destination: 1 }), + ) + win.webContents.send( + Channel.MAIN, + reorderedApps({ source: 4, destination: 2 }), + ) + + const updatedApps = screen.getAllByRole('button', { name: /[A-z]+ App/u }) + expect(updatedApps[0]).toHaveAttribute('aria-label', 'Safari App') + expect(updatedApps[1]).toHaveAttribute('aria-label', 'Opera App') + expect(updatedApps[2]).toHaveAttribute('aria-label', 'Brave App') + expect(updatedApps[3]).toHaveAttribute('aria-label', 'Firefox App') + expect(updatedApps[4]).toHaveAttribute('aria-label', 'Microsoft Edge App') +}) diff --git a/src/renderers/tiles/components/organisms/support-message.tsx b/src/renderers/picker/components/organisms/support-message.tsx similarity index 96% rename from src/renderers/tiles/components/organisms/support-message.tsx rename to src/renderers/picker/components/organisms/support-message.tsx index 70b99b32..e9190b7c 100644 --- a/src/renderers/tiles/components/organisms/support-message.tsx +++ b/src/renderers/picker/components/organisms/support-message.tsx @@ -22,8 +22,8 @@ const SupportMessage = (): JSX.Element => { >

- Thank you for downloading Browserosaurus. Please consider buying me a - coffee. + Thank you for downloading Browserosaurus. Please consider supporting + my open source projects.

Thank you{' '} diff --git a/src/renderers/tiles/components/organisms/url-bar.test.tsx b/src/renderers/picker/components/organisms/url-bar.test.tsx similarity index 100% rename from src/renderers/tiles/components/organisms/url-bar.test.tsx rename to src/renderers/picker/components/organisms/url-bar.test.tsx diff --git a/src/renderers/picker/components/organisms/url-bar.tsx b/src/renderers/picker/components/organisms/url-bar.tsx new file mode 100644 index 00000000..8c388308 --- /dev/null +++ b/src/renderers/picker/components/organisms/url-bar.tsx @@ -0,0 +1,68 @@ +import clsx from 'clsx' +import React from 'react' +import { useDispatch } from 'react-redux' +import Url from 'url' + +import { CARROT_URL } from '../../../../config/CONSTANTS' +import { clickedUrlBar } from '../../../../shared/state/actions' +import { useSelector } from '../../../../shared/state/hooks' + +interface Props { + className?: string +} + +const UrlBar: React.FC = ({ className }) => { + const dispatch = useDispatch() + const url = useSelector((state) => state.data.url) + const parsedUrl = Url.parse(url) + + return ( +

dispatch(clickedUrlBar())} + onKeyDown={() => false} + role="button" + tabIndex={-1} + title="Click to copy (⌘ + C)" + > +
+ {url === CARROT_URL && ( +
+ + ☕️ + {' '} + Choose a browser to open URL: +
+ )} + {parsedUrl.protocol} + {parsedUrl.slashes && '//'} + + {parsedUrl.host || Browserosaurus} + + + {parsedUrl.pathname} + {parsedUrl.search} + {parsedUrl.hash} + +
+
+ ) +} + +export default UrlBar diff --git a/src/renderers/tiles/index.html b/src/renderers/picker/index.html similarity index 100% rename from src/renderers/tiles/index.html rename to src/renderers/picker/index.html diff --git a/src/renderers/tiles/index.tsx b/src/renderers/picker/index.tsx similarity index 100% rename from src/renderers/tiles/index.tsx rename to src/renderers/picker/index.tsx diff --git a/src/renderers/picker/refs.ts b/src/renderers/picker/refs.ts new file mode 100644 index 00000000..78891b73 --- /dev/null +++ b/src/renderers/picker/refs.ts @@ -0,0 +1,3 @@ +import { createRef } from 'react' + +export const favAppRef = createRef() diff --git a/src/renderers/picker/state/middleware.ts b/src/renderers/picker/state/middleware.ts new file mode 100644 index 00000000..72476ab2 --- /dev/null +++ b/src/renderers/picker/state/middleware.ts @@ -0,0 +1,31 @@ +/* eslint-disable unicorn/prefer-regexp-test */ +import type { AnyAction, Middleware } from '@reduxjs/toolkit' + +import { clickedRestorePicker, urlOpened } from '../../../shared/state/actions' +import type { RootState } from '../../../shared/state/reducer.root' +import { favAppRef } from '../refs' + +/** + * Pass actions between main and renderers + */ +export const pickerMiddleware = + (): Middleware< + // Legacy type parameter added to satisfy interface signature + Record, + RootState + > => + () => + (next) => + (action: AnyAction) => { + /** + * Move to next middleware + */ + // eslint-disable-next-line node/callback-return -- must flush to get nextState + const result = next(action) + + if (urlOpened.match(action) || clickedRestorePicker.match(action)) { + favAppRef.current?.focus() + } + + return result + } diff --git a/src/renderers/picker/state/store.ts b/src/renderers/picker/state/store.ts new file mode 100644 index 00000000..92290695 --- /dev/null +++ b/src/renderers/picker/state/store.ts @@ -0,0 +1,18 @@ +import type { AnyAction } from '@reduxjs/toolkit' + +import { Channel } from '../../../shared/state/channels' +import createStore from '../../../shared/state/create-store' +import { customWindow } from '../../shared/custom.window' +import { busMiddleware } from '../../shared/state/middleware.bus' +import { pickerMiddleware } from './middleware' + +const store = createStore([busMiddleware(Channel.PICKER), pickerMiddleware()]) + +export default store + +/** + * Listen for all actions from main + */ +customWindow.electron.receive(Channel.MAIN, (action: AnyAction) => { + store.dispatch(action) +}) diff --git a/src/renderers/prefs/components/layout.tsx b/src/renderers/prefs/components/layout.tsx index afb09598..f3ef45ed 100644 --- a/src/renderers/prefs/components/layout.tsx +++ b/src/renderers/prefs/components/layout.tsx @@ -4,8 +4,8 @@ import { useDispatch } from 'react-redux' import { prefsStarted } from '../../../shared/state/actions' import { HeaderBar } from './organisms/header-bar' import { AboutPane } from './organisms/pane-about' +import { AppsPane } from './organisms/pane-apps' import { GeneralPane } from './organisms/pane-general' -import { AppsPane } from './organisms/pane-tiles' const useAppStarted = () => { const dispatch = useDispatch() diff --git a/src/renderers/prefs/components/organisms/header-bar.tsx b/src/renderers/prefs/components/organisms/header-bar.tsx index ec8d4479..47fc55e6 100644 --- a/src/renderers/prefs/components/organisms/header-bar.tsx +++ b/src/renderers/prefs/components/organisms/header-bar.tsx @@ -4,15 +4,40 @@ import { useDispatch } from 'react-redux' import { clickedTabButton } from '../../../../shared/state/actions' import { useSelector } from '../../../../shared/state/hooks' +import type { PrefsTab } from '../../../../shared/state/reducer.data' -interface Props { - className?: string +interface TabButtonProps { + tab: PrefsTab + children: string } -export const HeaderBar = ({ className }: Props): JSX.Element => { +const TabButton = ({ tab, children }: TabButtonProps) => { const dispatch = useDispatch() const prefsTab = useSelector((state) => state.data.prefsTab) + return ( + + ) +} + +interface HeaderBarProps { + className?: string +} + +export const HeaderBar = ({ className }: HeaderBarProps): JSX.Element => { return (
{ Browserosaurus Preferences
- - - + General + Apps + About
) diff --git a/src/renderers/prefs/components/organisms/pane-apps.tsx b/src/renderers/prefs/components/organisms/pane-apps.tsx new file mode 100644 index 00000000..bdc33f77 --- /dev/null +++ b/src/renderers/prefs/components/organisms/pane-apps.tsx @@ -0,0 +1,153 @@ +import { + ArrowSmDownIcon, + ArrowSmUpIcon, + StarIcon, + SwitchVerticalIcon, +} from '@heroicons/react/solid' +import clsx from 'clsx' +import React, { useCallback } from 'react' +import type { DropResult } from 'react-beautiful-dnd' +import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd' +import { useDispatch } from 'react-redux' + +import { apps } from '../../../../config/apps' +import { changedHotkey, reorderedApps } from '../../../../shared/state/actions' +import { useInstalledApps } from '../../../../shared/state/hooks' +import Input from '../../../shared/components/atoms/input' +import { Pane } from '../molecules/pane' + +interface DragDirectionArrowProps { + currentIndex: number + endIndex: number + className: string +} + +const DragDirectionArrow = ({ + currentIndex, + endIndex, + className, +}: DragDirectionArrowProps) => { + if (currentIndex === 0) { + return + } + + if (currentIndex === endIndex) { + return + } + + return +} + +export function AppsPane(): JSX.Element { + const dispatch = useDispatch() + + const installedApps = useInstalledApps() + + const onDragEnd = useCallback( + (result: DropResult) => { + if (!result.destination) { + return + } + + if (result.destination.index === result.source.index) { + return + } + + dispatch( + reorderedApps({ + source: result.source.index, + destination: result.destination.index, + }), + ) + }, + [dispatch], + ) + + return ( + + + + {(droppableProvided) => ( +
+ {installedApps.map(({ id, name, hotkey }, index) => ( + + {(draggableProvided, draggableSnapshop) => ( +
+
+ +
+
+ {index === 0 ? ( + + ) : ( + index + 1 + )} +
+
+ + {name} +
+
+ { + dispatch( + changedHotkey({ + appId: id, + value: event.currentTarget.value, + }), + ) + }} + onFocus={(event) => { + event.target.select() + }} + placeholder="Key" + type="text" + value={hotkey || ''} + /> +
+
+ )} +
+ ))} + {droppableProvided.placeholder} +
+ )} +
+
+
+ ) +} diff --git a/src/renderers/prefs/components/organisms/pane-general.tsx b/src/renderers/prefs/components/organisms/pane-general.tsx index 210eb5d8..87926c1b 100644 --- a/src/renderers/prefs/components/organisms/pane-general.tsx +++ b/src/renderers/prefs/components/organisms/pane-general.tsx @@ -43,7 +43,9 @@ export const GeneralPane = (): JSX.Element => { ) const updateStatus = useSelector((state) => state.data.updateStatus) - const numberOfInstalledApps = useSelector((state) => state.appIds.length) + const numberOfInstalledApps = useSelector( + (state) => state.storage.apps.length, + ) return ( diff --git a/src/renderers/prefs/components/organisms/pane-tiles.tsx b/src/renderers/prefs/components/organisms/pane-tiles.tsx deleted file mode 100644 index 2b462871..00000000 --- a/src/renderers/prefs/components/organisms/pane-tiles.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import clsx from 'clsx' -import React from 'react' -import { useDispatch } from 'react-redux' - -import { apps } from '../../../../config/apps' -import { - changedHotkey, - clickedEyeButton, - clickedFavButton, -} from '../../../../shared/state/actions' -import { useInstalledApps } from '../../../../shared/state/hooks' -import Button from '../../../shared/components/atoms/button' -import { - EyeIcon, - EyeOffIcon, - StarIcon, -} from '../../../shared/components/atoms/icons' -import Input from '../../../shared/components/atoms/input' -import { Pane } from '../molecules/pane' - -export function AppsPane(): JSX.Element { - const dispatch = useDispatch() - - const installedApps = useInstalledApps() - - return ( - -
-
Tile
-
Favourite
-
Visibility
-
Hotkey
-
-
- {installedApps.map(({ id, name, isVisible, isFav, hotkey }, index) => { - const isOdd = index % 2 !== 0 - return ( -
-
- - {name} -
-
- -
-
- -
-
- { - dispatch( - changedHotkey({ - appId: id, - value: event.currentTarget.value, - }), - ) - }} - onFocus={(event) => { - event.target.select() - }} - placeholder="Key" - type="text" - value={hotkey || ''} - /> -
-
- ) - })} -
-
- ) -} diff --git a/src/renderers/shared/components/atoms/button.tsx b/src/renderers/shared/components/atoms/button.tsx index 60461136..066f49e7 100644 --- a/src/renderers/shared/components/atoms/button.tsx +++ b/src/renderers/shared/components/atoms/button.tsx @@ -13,7 +13,7 @@ const Button: React.FC> = ({ className={clsx( className, disabled && 'opacity-40', - !disabled && 'focus:outline-none active:opacity-75', + !disabled && 'active:opacity-75', 'px-2 py-1', 'rounded-lg', 'leading-none', diff --git a/src/renderers/shared/components/atoms/icons.tsx b/src/renderers/shared/components/atoms/icons.tsx deleted file mode 100644 index cde23b04..00000000 --- a/src/renderers/shared/components/atoms/icons.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import clsx from 'clsx' -import React from 'react' - -interface Props { - className?: string -} - -export const GlobeIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const HomeIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const GiftIcon = ({ className = '' }: Props): JSX.Element => ( - - - - -) - -export const RefreshIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const LogoutIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const BackspaceIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const ClipboardCopyIcon = ({ className = '' }: Props): JSX.Element => ( - - - - -) - -export const XIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const CogIcon = ({ className = '' }: Props): JSX.Element => ( - - - -) - -export const StarIcon = ({ - className = '', - ...restProps -}: Props): JSX.Element => ( - - - -) - -export const EyeIcon = ({ className = '' }: Props): JSX.Element => ( - - - - -) - -export const EyeOffIcon = ({ className = '' }: Props): JSX.Element => ( - - - - -) diff --git a/src/renderers/shared/components/atoms/input.tsx b/src/renderers/shared/components/atoms/input.tsx index 805ed1f6..cf8c7360 100644 --- a/src/renderers/shared/components/atoms/input.tsx +++ b/src/renderers/shared/components/atoms/input.tsx @@ -8,7 +8,8 @@ const Input: React.FC> = ({ return ( { - const validChannels = [Channel.PREFS, Channel.TILES] + const validChannels = [Channel.PREFS, Channel.PICKER] if (validChannels.includes(channel)) { ipcRenderer.send(channel, action) } diff --git a/src/renderers/tiles/components/atoms/noop.tsx b/src/renderers/tiles/components/atoms/noop.tsx deleted file mode 100644 index d0062c7c..00000000 --- a/src/renderers/tiles/components/atoms/noop.tsx +++ /dev/null @@ -1,5 +0,0 @@ -const Noop = (): null => { - return null -} - -export default Noop diff --git a/src/renderers/tiles/components/layout.tsx b/src/renderers/tiles/components/layout.tsx deleted file mode 100644 index 623199b3..00000000 --- a/src/renderers/tiles/components/layout.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useEffect } from 'react' -import { useDispatch } from 'react-redux' - -import { tilesStarted } from '../../../shared/state/actions' -import { useKeyboardEvents } from './hooks/use-keyboard-events' -import SupportMessage from './organisms/support-message' -import Tiles from './organisms/tiles' -import UrlBar from './organisms/url-bar' - -const useAppStarted = () => { - const dispatch = useDispatch() - useEffect(() => { - dispatch(tilesStarted()) - }, [dispatch]) -} - -const App: React.FC = () => { - /** - * Tell main that renderer is available - */ - useAppStarted() - - /** - * Setup keyboard listeners - */ - useKeyboardEvents() - - return ( -
- - - -
- ) -} - -export default App diff --git a/src/renderers/tiles/components/molecules/tile.tsx b/src/renderers/tiles/components/molecules/tile.tsx deleted file mode 100644 index 64cc13c0..00000000 --- a/src/renderers/tiles/components/molecules/tile.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import clsx from 'clsx' -import React from 'react' -import { useDispatch } from 'react-redux' - -import { clickedTile } from '../../../../shared/state/actions' -import type { InstalledApp } from '../../../../shared/state/hooks' -import { useSelector } from '../../../../shared/state/hooks' -import { StarIcon } from '../../../shared/components/atoms/icons' -import AppLogo from '../atoms/app-logo' -import Kbd from '../atoms/kbd' - -interface Props { - app: InstalledApp -} - -const Tile: React.FC = ({ app }) => { - const dispatch = useDispatch() - const url = useSelector((state) => state.data.url) - - return ( -
- -
- ) -} - -export default Tile diff --git a/src/renderers/tiles/components/organisms/tiles.test.tsx b/src/renderers/tiles/components/organisms/tiles.test.tsx deleted file mode 100644 index c97315d7..00000000 --- a/src/renderers/tiles/components/organisms/tiles.test.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import '../../../shared/preload' - -import { act, fireEvent, render, screen, within } from '@testing-library/react' -import electron from 'electron' -import React from 'react' - -import { - clickedTile, - pressedAppKey, - syncAppIds, - syncStorage, - urlOpened, -} from '../../../../shared/state/actions' -import { Channel } from '../../../../shared/state/channels' -import Wrapper from '../_bootstrap' - -test('tiles', () => { - render() - const win = new electron.BrowserWindow() - act(() => { - win.webContents.send( - Channel.MAIN, - syncAppIds([ - 'org.mozilla.firefox', - 'com.apple.Safari', - 'com.brave.Browser.nightly', - ]), - ) - }) - // Check tiles and tile logos shown - expect(screen.getByAltText('Firefox')).toBeVisible() - expect(screen.getByRole('button', { name: 'Firefox Tile' })).toBeVisible() - expect(screen.getByAltText('Safari')).toBeVisible() - expect(screen.getByRole('button', { name: 'Safari Tile' })).toBeVisible() - expect(screen.getByAltText('Brave Nightly')).toBeVisible() - expect( - screen.getByRole('button', { name: 'Brave Nightly Tile' }), - ).toBeVisible() - - expect(screen.getAllByRole('button', { name: /[A-z]+ Tile/u })).toHaveLength( - 3, - ) - - act(() => { - win.webContents.send( - Channel.MAIN, - syncStorage({ - supportMessage: -1, - fav: 'com.apple.Safari', - hiddenTileIds: [], - hotkeys: {}, - width: 200, - height: 200, - firstRun: false, - }), - ) - }) - - // Set Safari as favourite - const safariTile = screen.getByRole('button', { name: 'Safari Tile' }) - expect(within(safariTile).getByLabelText('Star')).toBeVisible() - - // Correct info sent to main when tile clicked - fireEvent.click(screen.getByRole('button', { name: 'Firefox Tile' })) - expect(electron.ipcRenderer.send).toHaveBeenCalledWith( - Channel.TILES, - clickedTile({ - url: '', - appId: 'org.mozilla.firefox', - isAlt: false, - isShift: false, - }), - ) - - // Correct info sent to main when tile clicked - const url = 'http://example.com' - act(() => { - win.webContents.send(Channel.MAIN, urlOpened(url)) - }) - fireEvent.click(screen.getByRole('button', { name: 'Brave Nightly Tile' }), { - altKey: true, - }) - expect(electron.ipcRenderer.send).toHaveBeenCalledWith( - Channel.TILES, - clickedTile({ - url, - appId: 'com.brave.Browser.nightly', - isAlt: true, - isShift: false, - }), - ) -}) - -test('use hotkey', () => { - render() - const win = new electron.BrowserWindow() - act(() => { - win.webContents.send(Channel.MAIN, syncAppIds(['com.apple.Safari'])) - }) - act(() => { - win.webContents.send( - Channel.MAIN, - syncStorage({ - supportMessage: -1, - fav: 'com.apple.Safari', - hiddenTileIds: [], - hotkeys: { s: 'com.apple.Safari' }, - width: 200, - height: 200, - firstRun: false, - }), - ) - }) - - const url = 'http://example.com' - act(() => { - win.webContents.send(Channel.MAIN, urlOpened(url)) - }) - fireEvent.keyDown(document, { key: 'S', code: 'KeyS', keyCode: 83 }) - expect(electron.ipcRenderer.send).toHaveBeenCalledWith( - Channel.TILES, - pressedAppKey({ - url, - appId: 'com.apple.Safari', - isAlt: false, - isShift: false, - }), - ) -}) - -test('use hotkey with alt', () => { - render() - const win = new electron.BrowserWindow() - act(() => { - win.webContents.send(Channel.MAIN, syncAppIds(['com.apple.Safari'])) - }) - - act(() => { - win.webContents.send( - Channel.MAIN, - syncStorage({ - supportMessage: -1, - fav: 'com.apple.Safari', - hiddenTileIds: [], - hotkeys: { s: 'com.apple.Safari' }, - width: 200, - height: 200, - firstRun: false, - }), - ) - }) - - const url = 'http://example.com' - act(() => { - win.webContents.send(Channel.MAIN, urlOpened(url)) - }) - fireEvent.keyDown(document, { - key: 's', - code: 'KeyS', - keyCode: 83, - altKey: true, - }) - expect(electron.ipcRenderer.send).toHaveBeenCalledWith( - Channel.TILES, - pressedAppKey({ - url, - appId: 'com.apple.Safari', - isAlt: true, - isShift: false, - }), - ) -}) - -test('hold shift', () => { - render() - const win = new electron.BrowserWindow() - act(() => { - win.webContents.send(Channel.MAIN, syncAppIds(['org.mozilla.firefox'])) - }) - const url = 'http://example.com' - act(() => { - win.webContents.send(Channel.MAIN, urlOpened(url)) - }) - fireEvent.click(screen.getByRole('button', { name: 'Firefox Tile' }), { - shiftKey: true, - }) - expect(electron.ipcRenderer.send).toHaveBeenCalledWith( - Channel.TILES, - clickedTile({ - url, - appId: 'org.mozilla.firefox', - isAlt: false, - isShift: true, - }), - ) -}) - -test('tiles order', () => { - render() - const win = new electron.BrowserWindow() - act(() => { - win.webContents.send( - Channel.MAIN, - syncAppIds([ - 'org.mozilla.firefox', - 'com.apple.Safari', - 'com.operasoftware.Opera', - 'com.microsoft.edgemac', - 'com.brave.Browser', - ]), - ) - }) - // Check tiles and tile logos shown - const tiles = screen.getAllByRole('button', { name: /[A-z]+ Tile/u }) - - expect(tiles).toHaveLength(5) - - act(() => { - win.webContents.send( - Channel.MAIN, - syncStorage({ - supportMessage: -1, - fav: 'com.apple.Safari', - hiddenTileIds: [], - hotkeys: { 1: 'com.operasoftware.Opera', 2: 'com.brave.Browser' }, - width: 200, - height: 200, - firstRun: false, - }), - ) - }) - - const updatedTiles = screen.getAllByRole('button', { name: /[A-z]+ Tile/u }) - expect(updatedTiles[0]).toHaveAttribute('aria-label', 'Safari Tile') - expect(updatedTiles[1]).toHaveAttribute('aria-label', 'Opera Tile') - expect(updatedTiles[2]).toHaveAttribute('aria-label', 'Brave Tile') - expect(updatedTiles[3]).toHaveAttribute('aria-label', 'Firefox Tile') - expect(updatedTiles[4]).toHaveAttribute('aria-label', 'Microsoft Edge Tile') -}) diff --git a/src/renderers/tiles/components/organisms/tiles.tsx b/src/renderers/tiles/components/organisms/tiles.tsx deleted file mode 100644 index 522fad07..00000000 --- a/src/renderers/tiles/components/organisms/tiles.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react' - -import { useFavTile, useNormalTiles } from '../../../../shared/state/hooks' -import Tile from '../molecules/tile' - -const Tiles: React.FC = () => { - const favTile = useFavTile() - const normalTiles = useNormalTiles() - - return ( -
-
- {/* Favourite is first */} - {favTile && } - - {/* Rest of the tiles */} - {normalTiles.map((app, index) => { - const key = app.id + index - return - })} -
-
- ) -} - -export default Tiles diff --git a/src/renderers/tiles/components/organisms/url-bar.tsx b/src/renderers/tiles/components/organisms/url-bar.tsx deleted file mode 100644 index 2184a71c..00000000 --- a/src/renderers/tiles/components/organisms/url-bar.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import clsx from 'clsx' -import React from 'react' -import { useDispatch } from 'react-redux' -import Url from 'url' - -import { CARROT_URL } from '../../../../config/CONSTANTS' -import { - clickedCopyButton, - clickedUpdateAvailableButton, - clickedUrlBackspaceButton, -} from '../../../../shared/state/actions' -import { useSelector } from '../../../../shared/state/hooks' -import Button from '../../../shared/components/atoms/button' -import { - BackspaceIcon, - ClipboardCopyIcon, -} from '../../../shared/components/atoms/icons' - -interface Props { - className?: string -} - -const UrlBar: React.FC = ({ className }) => { - const dispatch = useDispatch() - const url = useSelector((state) => state.data.url) - const isUpdateAvailable = useSelector( - (state) => state.data.updateStatus === 'available', - ) - - const isEmpty = url.length === 0 - const parsedUrl = Url.parse(url) - - return ( -
-
-
- {url === CARROT_URL && ( -
- - ☕️ - {' '} - Choose a browser to open URL: -
- )} - {parsedUrl.protocol} - {parsedUrl.slashes && '//'} - - {parsedUrl.host || ( - Browserosaurus - )} - - - {parsedUrl.pathname} - {parsedUrl.search} - {parsedUrl.hash} - -
-
- -
-
- - - -
- - -
-
- ) -} - -export default UrlBar diff --git a/src/renderers/tiles/store.ts b/src/renderers/tiles/store.ts deleted file mode 100644 index bc41c8d1..00000000 --- a/src/renderers/tiles/store.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { AnyAction } from '@reduxjs/toolkit' - -import { Channel } from '../../shared/state/channels' -import createStore from '../../shared/state/create-store' -import { customWindow } from '../shared/custom.window' -import { busMiddleware } from '../shared/state/middleware.bus' - -const store = createStore([busMiddleware(Channel.TILES)]) - -export default store - -/** - * Listen for all actions from main - */ -customWindow.electron.receive(Channel.MAIN, (action: AnyAction) => { - store.dispatch(action) -}) diff --git a/src/shared/state/actions.ts b/src/shared/state/actions.ts index bec15941..de13e7ac 100644 --- a/src/shared/state/actions.ts +++ b/src/shared/state/actions.ts @@ -8,7 +8,7 @@ import type { Data, PrefsTab } from './reducer.data' import type { Storage } from './reducer.storage' const MAIN = Channel.MAIN -const TILES = Channel.TILES +const PICKER = Channel.PICKER const PREFS = Channel.PREFS // ----------------------------------------------------------------------------- @@ -18,8 +18,10 @@ const PREFS = Channel.PREFS const appReady = cA(`${MAIN}/appReady`) const urlOpened = cA(`${MAIN}/urlOpened`) -const tWindowBoundsChanged = cA(`${MAIN}/tWindowBoundsChanged`) -const syncAppIds = cA(`${MAIN}/syncAppIds`) +const pickerWindowBoundsChanged = cA( + `${MAIN}/pickerWinBoundsChanged`, +) +const installedAppsRetrieved = cA(`${MAIN}/installedAppsRetrieved`) const syncData = cA(`${MAIN}/syncData`) const syncStorage = cA(`${MAIN}/syncStorage`) @@ -30,8 +32,10 @@ const updateAvailable = cA(`${MAIN}/updateAvailable`) const updateDownloading = cA(`${MAIN}/updateDownloading`) const updateDownloaded = cA(`${MAIN}/updateDownloaded`) +const clickedRestorePicker = cA(`${MAIN}/clickedRestorePicker`) + // ----------------------------------------------------------------------------- -// TILES +// PICKER // ----------------------------------------------------------------------------- interface OpenAppArguments { @@ -41,9 +45,9 @@ interface OpenAppArguments { isShift: boolean } -const tilesStarted = cA(`${TILES}/started`) +const pickerStarted = cA(`${PICKER}/started`) -const clickedTile = cA(`${TILES}/clickTile`) +const clickedApp = cA(`${PICKER}/clickedApp`) const keydown = cA<{ isAlt: boolean @@ -52,18 +56,16 @@ const keydown = cA<{ code: string key: string keyCode: number -}>(`${TILES}/keydown`) -const pressedEscapeKey = cA(`${TILES}/escapeKey`) -const pressedBackspaceKey = cA(`${TILES}/backspaceKey`) -const pressedCopyKey = cA(`${TILES}/copyKey`) -const pressedAppKey = cA(`${TILES}/appKey`) +}>(`${PICKER}/keydown`) +const pressedEscapeKey = cA(`${PICKER}/escapeKey`) +const pressedBackspaceKey = cA(`${PICKER}/backspaceKey`) +const pressedCopyKey = cA(`${PICKER}/copyKey`) +const pressedAppKey = cA(`${PICKER}/appKey`) -const clickedUrlBackspaceButton = cA(`${TILES}/clickedUrlBackspaceButton`) -const clickedCopyButton = cA(`${TILES}/clickedCopyButton`) -const clickedUpdateAvailableButton = cA(`${TILES}/clickedUpdateAvailableButton`) +const clickedUrlBar = cA(`${PICKER}/clickedUrlBar`) -const clickedDonate = cA(`${TILES}/clickedDonate`) -const clickedMaybeLater = cA(`${TILES}/clickedMaybeLater`) +const clickedDonate = cA(`${PICKER}/clickedDonate`) +const clickedMaybeLater = cA(`${PICKER}/clickedMaybeLater`) // ----------------------------------------------------------------------------- // PREFS @@ -80,8 +82,6 @@ const clickedRescanApps = cA(`${PREFS}/clickedRescanApps`) const clickedUpdateButton = cA(`${PREFS}/clickedUpdateButton`) const clickedUpdateRestartButton = cA(`${PREFS}/clickedUpdateRestartButton`) -const clickedFavButton = cA(`${PREFS}/clickedFavButton`) -const clickedEyeButton = cA(`${PREFS}/clickedEyeButton`) const changedHotkey = cA<{ appId: AppId; value: string }>( `${PREFS}/changedHotkey`, ) @@ -89,37 +89,39 @@ const changedHotkey = cA<{ appId: AppId; value: string }>( const clickedHomepageButton = cA(`${PREFS}/clickedHomepageButton`) const clickedOpenIssueButton = cA(`${PREFS}/clickedOpenIssueButton`) +const reorderedApps = cA<{ source: number; destination: number }>( + `${PREFS}/reorderedApps`, +) + export { appReady, changedHotkey, - clickedCopyButton, + clickedApp, clickedDonate, - clickedEyeButton, - clickedFavButton, clickedHomepageButton, clickedMaybeLater, clickedOpenIssueButton, clickedRescanApps, + clickedRestorePicker, clickedSetAsDefaultBrowserButton, clickedTabButton, - clickedTile, - clickedUpdateAvailableButton, clickedUpdateButton, clickedUpdateRestartButton, - clickedUrlBackspaceButton, + clickedUrlBar, gotAppVersion, gotDefaultBrowserStatus, + installedAppsRetrieved, keydown, + pickerStarted, + pickerWindowBoundsChanged, prefsStarted, pressedAppKey, pressedBackspaceKey, pressedCopyKey, pressedEscapeKey, - syncAppIds, + reorderedApps, syncData, syncStorage, - tilesStarted, - tWindowBoundsChanged, updateAvailable, updateDownloaded, updateDownloading, diff --git a/src/shared/state/channels.ts b/src/shared/state/channels.ts index b013b406..cd09463d 100644 --- a/src/shared/state/channels.ts +++ b/src/shared/state/channels.ts @@ -1,5 +1,5 @@ export const enum Channel { PREFS = 'PREFS', - TILES = 'TILES', + PICKER = 'PICKER', MAIN = 'MAIN', } diff --git a/src/shared/state/hooks.ts b/src/shared/state/hooks.ts index 5cbb4ec8..6d51f50c 100644 --- a/src/shared/state/hooks.ts +++ b/src/shared/state/hooks.ts @@ -4,7 +4,6 @@ import { shallowEqual, useSelector as useReduxSelector } from 'react-redux' import type { AppId, Apps } from '../../config/apps' import { apps as allApps } from '../../config/apps' -import { getHotkeyByAppId } from '../utils/get-hotkey-by-app-id' import type { RootState } from './reducer.root' export const useSelector: TypedUseSelectorHook = useReduxSelector @@ -22,55 +21,18 @@ export interface InstalledApp { name: Apps[AppId]['name'] privateArg?: string urlTemplate?: string - isVisible: boolean - isFav: boolean - hotkey: string | undefined + hotkey: string | null } export const useInstalledApps = (): InstalledApp[] => { - const installedAppIds = useDeepEqualSelector((state) => state.appIds) - const hiddenTileIds = useShallowEqualSelector( - (state) => state.storage.hiddenTileIds, - ) - const favId = useSelector((state) => state.storage.fav) - const hotkeys = useShallowEqualSelector((state) => state.storage.hotkeys) - return installedAppIds.map((appId) => ({ - ...allApps[appId], - id: appId, - isVisible: !hiddenTileIds.includes(appId), - isFav: appId === favId, - hotkey: getHotkeyByAppId(hotkeys, appId), + const installedAppIds = useDeepEqualSelector((state) => state.storage.apps) + return installedAppIds.map((installedApp) => ({ + ...allApps[installedApp.id], + id: installedApp.id, + hotkey: installedApp.hotkey, })) } -/** - * Tiles = visible installed apps - */ -const useTiles = (): InstalledApp[] => { - const apps = useInstalledApps() - return apps.filter((app) => app.isVisible) -} - -export const useFavTile = (): InstalledApp | undefined => { - const tiles = useTiles() - const favTile = tiles.find((tile) => tile.isFav) - return favTile -} - -export const useNormalTiles = (): InstalledApp[] => { - const tiles = useTiles() - const normalTiles = tiles - .filter((tile) => !tile.isFav) - .sort((a, b) => { - if (!a.hotkey) return 1 - if (!b.hotkey) return -1 - if (a.hotkey < b.hotkey) return -1 - if (a.hotkey > b.hotkey) return 1 - return 0 - }) - return normalTiles -} - export const useIsSupportMessageHidden = (): boolean => { const supportMessageNumber = useSelector( (state) => state.storage.supportMessage, diff --git a/src/shared/state/reducer.app-ids.ts b/src/shared/state/reducer.app-ids.ts deleted file mode 100644 index 91797237..00000000 --- a/src/shared/state/reducer.app-ids.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { createReducer } from '@reduxjs/toolkit' - -import type { AppId } from '../../config/apps' -import { syncAppIds } from './actions' - -export const defaultAppIds: AppId[] = [] - -export const appIds = createReducer(defaultAppIds, (builder) => - builder.addCase(syncAppIds, (_, action) => action.payload), -) diff --git a/src/shared/state/reducer.data.ts b/src/shared/state/reducer.data.ts index 4e75655e..0bfe9cf8 100644 --- a/src/shared/state/reducer.data.ts +++ b/src/shared/state/reducer.data.ts @@ -5,28 +5,26 @@ import { backspaceUrlParse } from '../utils/backspace-url-parse' import { clickedDonate, clickedTabButton, - clickedUpdateAvailableButton, - clickedUrlBackspaceButton, gotAppVersion, gotDefaultBrowserStatus, + pickerStarted, prefsStarted, pressedBackspaceKey, syncData, - tilesStarted, updateAvailable, updateDownloaded, updateDownloading, urlOpened, } from './actions' -export type PrefsTab = 'about' | 'general' | 'tiles' +export type PrefsTab = 'about' | 'apps' | 'general' export interface Data { version: string updateStatus: 'available' | 'downloaded' | 'downloading' | 'no-update' isDefaultProtocolClient: boolean url: string - tilesStarted: boolean + pickerStarted: boolean prefsStarted: boolean prefsTab: PrefsTab } @@ -36,7 +34,7 @@ export const defaultData: Data = { updateStatus: 'no-update', isDefaultProtocolClient: true, url: '', - tilesStarted: false, + pickerStarted: false, prefsStarted: false, prefsTab: 'general', } @@ -45,18 +43,14 @@ export const data = createReducer(defaultData, (builder) => builder .addCase(syncData, (_, action) => action.payload) - .addCase(tilesStarted, (state) => { - state.tilesStarted = true + .addCase(pickerStarted, (state) => { + state.pickerStarted = true }) .addCase(prefsStarted, (state) => { state.prefsStarted = true }) - .addCase(clickedUrlBackspaceButton, (state) => { - state.url = backspaceUrlParse(state.url) - }) - .addCase(pressedBackspaceKey, (state) => { state.url = backspaceUrlParse(state.url) }) @@ -91,9 +85,5 @@ export const data = createReducer(defaultData, (builder) => .addCase(clickedTabButton, (state, action) => { state.prefsTab = action.payload - }) - - .addCase(clickedUpdateAvailableButton, (state) => { - state.prefsTab = 'general' }), ) diff --git a/src/shared/state/reducer.root.ts b/src/shared/state/reducer.root.ts index 1937cd7d..5477cfe8 100644 --- a/src/shared/state/reducer.root.ts +++ b/src/shared/state/reducer.root.ts @@ -1,16 +1,14 @@ import type { AnyAction, ThunkAction } from '@reduxjs/toolkit' import { combineReducers } from '@reduxjs/toolkit' -import { appIds, defaultAppIds } from './reducer.app-ids' import { data, defaultData } from './reducer.data' import { defaultStorage, storage } from './reducer.storage' -export const rootReducer = combineReducers({ data, storage, appIds }) +export const rootReducer = combineReducers({ data, storage }) export type RootState = ReturnType export const defaultState: RootState = { - appIds: defaultAppIds, data: defaultData, storage: defaultStorage, } diff --git a/src/shared/state/reducer.storage.ts b/src/shared/state/reducer.storage.ts index 2cc4aa15..9cbb0134 100644 --- a/src/shared/state/reducer.storage.ts +++ b/src/shared/state/reducer.storage.ts @@ -1,37 +1,27 @@ import { createReducer } from '@reduxjs/toolkit' -import xor from 'lodash/xor' import type { AppId } from '../../config/apps' -import { alterHotkeys } from '../utils/alter-hotkeys' import { changedHotkey, clickedDonate, - clickedEyeButton, - clickedFavButton, clickedMaybeLater, + installedAppsRetrieved, + pickerWindowBoundsChanged, + reorderedApps, syncStorage, - tWindowBoundsChanged, } from './actions' -export type Hotkeys = Record - export interface Storage { + apps: { id: AppId; hotkey: string | null }[] supportMessage: number - fav: AppId firstRun: boolean - hotkeys: Hotkeys - hiddenTileIds: AppId[] - width: number height: number } export const defaultStorage: Storage = { + apps: [], supportMessage: 0, - fav: 'com.apple.Safari', firstRun: true, - hotkeys: {}, - hiddenTileIds: [], - width: 424, height: 204, } @@ -39,23 +29,40 @@ export const storage = createReducer(defaultStorage, (builder) => builder .addCase(syncStorage, (state, action) => action.payload) - .addCase(changedHotkey, (state, action) => { - const updatedHotkeys = alterHotkeys( - state.hotkeys, - action.payload.appId, - action.payload.value, + .addCase(installedAppsRetrieved, (state, action) => { + const installedAppIds = action.payload + + const installedStoredApps = state.apps.filter((storedApp) => + installedAppIds.includes(storedApp.id), ) - state.hotkeys = updatedHotkeys - }) - .addCase(clickedEyeButton, (state, action) => { - const { hiddenTileIds } = state - // Remove the id if it exists in the array, or add it if it doesn't - state.hiddenTileIds = xor(hiddenTileIds, [action.payload]) + const notStoredInstalledApps = installedAppIds + .filter( + (id) => + !installedStoredApps + .map((installedStoredApp) => installedStoredApp.id) + .includes(id), + ) + .map((id) => ({ id, hotkey: null })) + + state.apps = [...installedStoredApps, ...notStoredInstalledApps] }) - .addCase(clickedFavButton, (state, action) => { - state.fav = action.payload + .addCase(changedHotkey, (state, action) => { + const lowerHotkey = action.payload.value.toLowerCase() + const appWithSameHotkeyIndex = state.apps.findIndex( + (app) => app.hotkey === lowerHotkey, + ) + + if (appWithSameHotkeyIndex > -1) { + state.apps[appWithSameHotkeyIndex].hotkey = null + } + + const appIndex = state.apps.findIndex( + (app) => app.id === action.payload.appId, + ) + + state.apps[appIndex].hotkey = lowerHotkey }) .addCase(clickedDonate, (state) => { @@ -66,8 +73,12 @@ export const storage = createReducer(defaultStorage, (builder) => state.supportMessage = Date.now() }) - .addCase(tWindowBoundsChanged, (state, action) => { - state.width = action.payload.width + .addCase(pickerWindowBoundsChanged, (state, action) => { state.height = action.payload.height + }) + + .addCase(reorderedApps, (state, action) => { + const [removed] = state.apps.splice(action.payload.source, 1) + state.apps.splice(action.payload.destination, 0, removed) }), ) diff --git a/src/shared/utils/action-logger.ts b/src/shared/utils/action-logger.ts index 9c6fcb39..d35a4d6a 100644 --- a/src/shared/utils/action-logger.ts +++ b/src/shared/utils/action-logger.ts @@ -8,7 +8,7 @@ import { Channel } from '../state/channels' const colorMap = { [Channel.MAIN]: yellow, [Channel.PREFS]: blue, - [Channel.TILES]: magenta, + [Channel.PICKER]: magenta, } export function actionLogger(action: AnyAction): void { @@ -16,7 +16,7 @@ export function actionLogger(action: AnyAction): void { console.log() console.log( - `${bold(colorMap[channel](channel.padEnd(5)))} ${bold(white(name))}`, + `${bold(colorMap[channel](channel.padEnd(6)))} ${bold(white(name))}`, ) console.log(action.payload) console.log() diff --git a/src/shared/utils/alter-hotkeys.test.ts b/src/shared/utils/alter-hotkeys.test.ts deleted file mode 100644 index 40de1736..00000000 --- a/src/shared/utils/alter-hotkeys.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import type { AppId } from '../../config/apps' -import type { Hotkeys } from '../state/reducer.storage' -import { alterHotkeys } from './alter-hotkeys' - -const cases: [Hotkeys, AppId, string, Hotkeys][] = [ - // Add Safari to empty hotkeys - [{}, 'com.apple.Safari', 's', { s: 'com.apple.Safari' }], - // Make sure case of hotkey is not relevant - [{}, 'com.apple.Safari', 'S', { s: 'com.apple.Safari' }], - // Change a hotkey - [ - { s: 'com.apple.Safari' }, - 'com.apple.Safari', - 'd', - { d: 'com.apple.Safari' }, - ], - // Remove a hotkey - [{ s: 'com.apple.Safari' }, 'com.apple.Safari', '', {}], - [ - { s: 'com.apple.Safari', f: 'org.mozilla.firefox' }, - 'com.apple.Safari', - '', - { f: 'org.mozilla.firefox' }, - ], - // Add to hotkeys - [ - { s: 'com.apple.Safari' }, - 'org.mozilla.firefox', - 'f', - { s: 'com.apple.Safari', f: 'org.mozilla.firefox' }, - ], - // Use a number as hotkey - [ - { s: 'com.apple.Safari' }, - 'com.apple.Safari', - '2', - { 2: 'com.apple.Safari' }, - ], - // Move a hotkey over to a different app - [ - { s: 'com.apple.Safari' }, - 'org.mozilla.firefox', - 's', - { s: 'org.mozilla.firefox' }, - ], - // Move a hotkey over to a different app - [ - { s: 'com.apple.Safari', f: 'org.mozilla.firefox' }, - 'org.mozilla.firefox', - 's', - { s: 'org.mozilla.firefox' }, - ], - // Move a hotkey over to a different app - [ - { - s: 'com.apple.Safari', - f: 'org.mozilla.firefox', - c: 'org.chromium.Chromium', - }, - 'org.mozilla.firefox', - 'c', - { - s: 'com.apple.Safari', - c: 'org.mozilla.firefox', - }, - ], - // Do nothing on non alphanumeric - [ - { s: 'com.apple.Safari' }, - 'com.apple.Safari', - '-', - { s: 'com.apple.Safari' }, - ], - // Do nothing on more than a single character - [ - { s: 'com.apple.Safari' }, - 'com.apple.Safari', - 'aa', - { s: 'com.apple.Safari' }, - ], -] - -test.each(cases)( - 'given hotkeys %p, app ID %p, and hotkey %p return %p', - (hotkeys, appId, hotkey, expected) => { - expect(alterHotkeys(hotkeys, appId, hotkey)).toStrictEqual(expected) - }, -) diff --git a/src/shared/utils/alter-hotkeys.ts b/src/shared/utils/alter-hotkeys.ts deleted file mode 100644 index 5da222f7..00000000 --- a/src/shared/utils/alter-hotkeys.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { AppId } from '../../config/apps' -import type { Hotkeys } from '../state/reducer.storage' -import { getHotkeyByAppId } from './get-hotkey-by-app-id' - -// Update a hotkeys object based on incoming app ID and hotkey combo -export function alterHotkeys( - hotkeys: Hotkeys, - appId: AppId, - hotkey: string, -): Hotkeys { - const lowerHotkey = hotkey.toLowerCase() - - // Do not alter original hotkeys object - const hotkeysCopy = { ...hotkeys } - - // Find the previous key for this app - const oldKey = getHotkeyByAppId(hotkeysCopy, appId) - - // If the new hotkey is empty, it's a deletion and so remove the current entry - if (!lowerHotkey) { - delete hotkeysCopy[oldKey || ''] - return hotkeysCopy - } - - // If the new key is allowed, delete the previous entry and add new entry - const matchAlphaNumeric = lowerHotkey.match(/^([a-z0-9])$/u) - if (matchAlphaNumeric) { - delete hotkeysCopy[oldKey || ''] - return { ...hotkeysCopy, [lowerHotkey]: appId } - } - - // Else change nothing and return the original - return hotkeys -} diff --git a/src/shared/utils/get-hotkey-by-app-id.test.ts b/src/shared/utils/get-hotkey-by-app-id.test.ts deleted file mode 100644 index bd1d091f..00000000 --- a/src/shared/utils/get-hotkey-by-app-id.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { AppId } from '../../config/apps' -import type { Hotkeys } from '../state/reducer.storage' -import { getHotkeyByAppId } from './get-hotkey-by-app-id' - -const cases: [Hotkeys, AppId, ReturnType][] = [ - // Does not exist - // @ts-expect-error -- missing app ID - [{}, '', undefined], - // @ts-expect-error -- unknown app ID - [{}, 'com.example', undefined], - [{ s: 'com.apple.Safari' }, 'com.microsoft.edgemac', undefined], - // Exists - [{ s: 'com.apple.Safari' }, 'com.apple.Safari', 's'], - [ - { s: 'com.apple.Safari', e: 'com.microsoft.edgemac' }, - 'com.apple.Safari', - 's', - ], - [ - { s: 'com.apple.Safari', e: 'com.microsoft.edgemac' }, - 'com.microsoft.edgemac', - 'e', - ], -] - -test.each(cases)('given %p return %p', (hotkeys, appId, expected) => { - expect(getHotkeyByAppId(hotkeys, appId)).toBe(expected) -}) diff --git a/src/shared/utils/get-hotkey-by-app-id.ts b/src/shared/utils/get-hotkey-by-app-id.ts deleted file mode 100644 index 1deb6757..00000000 --- a/src/shared/utils/get-hotkey-by-app-id.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { AppId } from '../../config/apps' -import type { Hotkeys } from '../state/reducer.storage' - -export function getHotkeyByAppId( - hotkeys: Hotkeys, - appId: AppId, -): string | undefined { - return Object.keys(hotkeys).find((key) => hotkeys[key] === appId) -}